Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ history, authorship, licenses, and dependency notices.

## Current component

Web Console [1.6.196](https://github.com/PastureStack/web-console/releases/tag/1.6.196)
is published with optional AI collaboration: seven typed tools, human upgrade
approval, conditional description edits and a dedicated-Key stdio bridge.
Browser tools start paused and require an explicit user choice. The
[release note](docs/releases/web-console-1.6.196.md) records the producer,
same-tree release tag, passing CI and verified artifact checksums.
Web Console [1.6.197](https://github.com/PastureStack/web-console/releases/tag/1.6.197)
restores the verified browser actor when an existing cookie is opened in a
fresh browser session. This allows the human approval inbox to work without
requiring a new login. Optional AI collaboration still provides seven typed
tools, conditional description edits and a dedicated-Key stdio bridge.
Browser tools start paused and require an explicit user choice. See the
[release note](docs/releases/web-console-1.6.197.md) for the session boundary.

The release producer uses Node.js `24.22.1` / npm `12.0.2` in a digest-pinned
container and produces an immutable static archive with separate runtime and
Expand Down
50 changes: 48 additions & 2 deletions app/services/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export default Service.extend({
if ( !cookie ) {
return reject({status: 401, message: 'No session cookie'});
}
let beforeValidation = this.get('authSession').readShared();

return this._readCurrentToken().then((token) => {
return this.get('authSession').runExclusive(() => {
Expand All @@ -260,10 +261,21 @@ export default Service.extend({

let current = this.get('authSession').readShared();
if ( !current ) {
if ( beforeValidation ) {
return reject({status: 409, message: 'Session changed during validation'});
}
current = this.get('authSession').commit(
this.get('authSession').createGeneration(), token.accountId, cookie
);
} else {
if ( current.accountId === null ) {
if ( !beforeValidation || current.generation !== beforeValidation.generation ) {
return reject({status: 409, message: 'Session changed during validation'});
}
current = this.get('authSession').commit(current.generation, token.accountId, cookie);
} else if ( current.accountId !== token.accountId ) {
return reject({status: 409, message: 'Session account changed during validation'});
}
this.get('authSession').adopt(current, cookie);
}
this._applyTokenMetadata(token);
Expand Down Expand Up @@ -462,6 +474,7 @@ export default Service.extend({
},

_readCurrentToken() {
let cookie = this.get('cookies').get(C.COOKIE.TOKEN);
return this.get('userStore').rawRequest({
url: 'token',
}).then((xhr) => {
Expand All @@ -482,7 +495,28 @@ export default Service.extend({
if ( !token || (!hasAccountId && !hasUser && !hasUserIdentity) ) {
return reject({status: 401, message: 'No authenticated session'});
}
return token;
const accountIdPattern = /^[a-zA-Z0-9_-]{1,256}$/;
if ( hasAccountId && !accountIdPattern.test(accountId) ) {
return reject({status: 401, message: 'Invalid authenticated account'});
}
if ( hasAccountId ) { return token; }
if ( this.get('cookies').get(C.COOKIE.TOKEN) !== cookie ) {
return reject({status: 409, message: 'Session changed during validation'});
}
// /token uses a virtual policy account in its response headers. After
// validating its identity, read the real actor from the authenticated
// API root; project account headers identify environments, not users.
return this.get('userStore').rawRequest({url: ''}).then((actorResponse) => {
if ( this.get('cookies').get(C.COOKIE.TOKEN) !== cookie ) {
return reject({status: 409, message: 'Session changed during validation'});
}
let header = actorResponse.headers && typeof actorResponse.headers.get === 'function' ? actorResponse.headers.get(C.HEADER.USER_ID) : null;
let verifiedAccountId = typeof header === 'string' ? header.trim() : '';
if ( !accountIdPattern.test(verifiedAccountId) ) {
return reject({status: 401, message: 'No authenticated account'});
}
return Object.assign({}, token, {accountId: verifiedAccountId});
});
});
},

Expand Down Expand Up @@ -515,17 +549,29 @@ export default Service.extend({
if ( !current || current.generation !== shared.generation || currentCookie !== cookie ) {
return {status: 'stale', generation: current && current.generation};
}
if ( current.accountId === null ) {
current = this.get('authSession').commit(current.generation, token.accountId, currentCookie);
} else if ( current.accountId !== token.accountId ) {
return reject({status: 409, message: 'Session account changed during validation'});
}
this.get('authSession').adopt(current, currentCookie);
this._applyTokenMetadata(token);
return {status: 'adopted', generation: current.generation};
});
}, (error) => {
if ( this._errorStatus(error) !== 401 ) {
let status = this._errorStatus(error);
if ( status !== 401 && status !== 409 ) {
return reject(error);
}
return this.get('authSession').runExclusive(() => {
let current = this.get('authSession').readShared();
let currentCookie = this.get('cookies').get(C.COOKIE.TOKEN);
if ( status === 409 ) {
if ( !current || current.generation !== shared.generation || currentCookie !== cookie ) {
return {status: 'stale', generation: current && current.generation};
}
return reject(error);
}
if ( current && current.generation === shared.generation && currentCookie === cookie ) {
this._clearOwnedLocalState(shared.generation);
return {status: 'invalid'};
Expand Down
1 change: 1 addition & 0 deletions app/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ var C = {

HEADER: {
ACCOUNT_ID: 'X-Api-Account-Id',
USER_ID: 'X-Api-User-Id',
ACTIONS: 'X-Api-Action-Links',
ACTIONS_VALUE: 'actionLinks',
CSRF: 'X-Api-Csrf',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.196",
"version": "1.6.197",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.196",
"version": "1.6.197",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
24 changes: 24 additions & 0 deletions docs/releases/web-console-1.6.197.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Web Console 1.6.197

This maintenance release repairs actor restoration when a valid cookie opens
a fresh browser session. The current-token response can contain a verified
identity while omitting `accountId`; its policy headers identify a virtual
token account. After identity validation, the console reads the authenticated
API root's `X-Api-User-Id` to recover the real actor. It does not decode the JWT
or use a client-supplied identity as an account ID.

A legacy shared session with a null actor is repaired only while its cookie
and generation still match under the existing session mutex. Delayed replies
cannot replace a newer login. Login-options responses remain unauthenticated,
and permission failures keep their existing handling. Ordinary credential and
MFA login flows are unchanged.

The AI collaboration build ceiling remains an explicit producer option, and
each browser page still starts with tools paused. Human approval remains a
normal authenticated UI action. The standalone bridge 0.1.0 and its shared
command-engine sources are unchanged; this release does not republish it.

The [release assets](https://github.com/PastureStack/web-console/releases/tag/1.6.197)
include the static archive, source/profile manifest, checksums and separate
runtime/build SBOMs. Server assembly and deployed-runtime acceptance remain
separate from the browser component release.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.196",
"version": "1.6.197",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.196" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.196"
if [[ "$version" != "1.6.197" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.197"
failures=$((failures + 1))
fi

Expand Down
Loading
Loading