@amalgm/core 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PURPOSE.md CHANGED
@@ -122,9 +122,14 @@ source until each module is cut over.
122
122
  re-resolved against the verified catalog on every resume, and a changed
123
123
  plan becomes durable only after its installation and runtime prove healthy.
124
124
  22. **User access is short-lived and machine-bound.** A durable machine
125
- credential may mint an audience- and scope-exact access token, but every
126
- resource request also proves possession of that machine's key; Core owns
127
- the claims and renewal law while hosts own keys, storage, signing, and HTTP.
125
+ credential may mint an audience- and scope-exact access token, but every
126
+ resource request also proves possession of that machine's key; Core owns
127
+ the claims and renewal law while hosts own keys, storage, signing, and HTTP.
128
+ 23. **Machine key replacement is an explicit recovery, never startup repair.**
129
+ Ordinary registration may pin an absent key or reuse the same key; replacing
130
+ it requires fresh user approval, proof of the candidate private key, and a
131
+ conditional swap from the expected old thumbprint. A completed swap is safe
132
+ to retry, and corrupt local key material remains visible until that ceremony.
128
133
 
129
134
  ## Five surfaces, one behavior
130
135
 
package/README.md CHANGED
@@ -32,3 +32,8 @@ Rules of the house: zero runtime dependencies; nothing in `src/` imports
32
32
  shipped engine (the production source until cutover — PURPOSE.md axiom 9);
33
33
  tests are exams — each states a law, and a mutant must be convicted by at
34
34
  least one.
35
+
36
+ Package releases use `.github/workflows/publish-package.yml`. A reviewed main
37
+ commit is tagged exactly `v<package version>`; trusted npm publishing stages
38
+ that package, and a maintainer approves the staged artifact with 2FA. Manual
39
+ dirty-worktree publishing is not a release path.
@@ -2,4 +2,5 @@ export * from './policy.js';
2
2
  export * from './claims.js';
3
3
  export * from './dpop.js';
4
4
  export * from './broker.js';
5
+ export * from './key-transition.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/identity/authorization/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/identity/authorization/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
@@ -2,4 +2,5 @@ export * from './policy.js';
2
2
  export * from './claims.js';
3
3
  export * from './dpop.js';
4
4
  export * from './broker.js';
5
+ export * from './key-transition.js';
5
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/identity/authorization/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/identity/authorization/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,19 @@
1
+ export declare const AUTHORIZATION_KEY_RECOVERY_PURPOSE: "authorization_key_recovery";
2
+ export type AuthorizationKeyTransition = 'pin' | 'unchanged' | 'rotate';
3
+ export interface AuthorizationKeyTransitionInput {
4
+ readonly currentKeyThumbprint: string | null;
5
+ readonly presentedKeyThumbprint: string;
6
+ readonly recovery?: Readonly<{
7
+ readonly purpose: typeof AUTHORIZATION_KEY_RECOVERY_PURPOSE;
8
+ readonly expectedKeyThumbprint: string;
9
+ }>;
10
+ }
11
+ /** One closed rule for initial pinning, ordinary reuse, and explicit recovery. */
12
+ export declare function authorizationKeyTransition(input: AuthorizationKeyTransitionInput): AuthorizationKeyTransition;
13
+ export declare function isAuthorizationKeyThumbprint(value: unknown): value is string;
14
+ export type AuthorizationKeyTransitionErrorCode = 'invalid_key_thumbprint' | 'invalid_recovery_purpose' | 'recovery_key_unchanged' | 'recovery_basis_mismatch' | 'key_mismatch';
15
+ export declare class AuthorizationKeyTransitionError extends Error {
16
+ readonly code: AuthorizationKeyTransitionErrorCode;
17
+ constructor(code: AuthorizationKeyTransitionErrorCode);
18
+ }
19
+ //# sourceMappingURL=key-transition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-transition.d.ts","sourceRoot":"","sources":["../../../src/identity/authorization/key-transition.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,EAAG,4BAAqC,CAAC;AAExF,MAAM,MAAM,0BAA0B,GAAG,KAAK,GAAG,WAAW,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,kCAAkC,CAAC;QAC5D,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;KACxC,CAAC,CAAC;CACJ;AAED,kFAAkF;AAClF,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,+BAA+B,GACrC,0BAA0B,CAsB5B;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5E;AAQD,MAAM,MAAM,mCAAmC,GAC3C,wBAAwB,GACxB,0BAA0B,GAC1B,wBAAwB,GACxB,yBAAyB,GACzB,cAAc,CAAC;AAEnB,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,IAAI,EAAE,mCAAmC,CAAC;gBAEvC,IAAI,EAAE,mCAAmC;CAKtD"}
@@ -0,0 +1,44 @@
1
+ export const AUTHORIZATION_KEY_RECOVERY_PURPOSE = 'authorization_key_recovery';
2
+ /** One closed rule for initial pinning, ordinary reuse, and explicit recovery. */
3
+ export function authorizationKeyTransition(input) {
4
+ assertKeyThumbprint(input.presentedKeyThumbprint);
5
+ if (input.currentKeyThumbprint !== null)
6
+ assertKeyThumbprint(input.currentKeyThumbprint);
7
+ if (input.recovery) {
8
+ assertKeyThumbprint(input.recovery.expectedKeyThumbprint);
9
+ if (input.recovery.purpose !== AUTHORIZATION_KEY_RECOVERY_PURPOSE) {
10
+ throw new AuthorizationKeyTransitionError('invalid_recovery_purpose');
11
+ }
12
+ if (input.presentedKeyThumbprint === input.recovery.expectedKeyThumbprint) {
13
+ throw new AuthorizationKeyTransitionError('recovery_key_unchanged');
14
+ }
15
+ if (input.currentKeyThumbprint === input.presentedKeyThumbprint)
16
+ return 'unchanged';
17
+ if (input.currentKeyThumbprint !== input.recovery.expectedKeyThumbprint) {
18
+ throw new AuthorizationKeyTransitionError('recovery_basis_mismatch');
19
+ }
20
+ return 'rotate';
21
+ }
22
+ if (input.currentKeyThumbprint === null)
23
+ return 'pin';
24
+ if (input.currentKeyThumbprint === input.presentedKeyThumbprint)
25
+ return 'unchanged';
26
+ throw new AuthorizationKeyTransitionError('key_mismatch');
27
+ }
28
+ export function isAuthorizationKeyThumbprint(value) {
29
+ return typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value);
30
+ }
31
+ function assertKeyThumbprint(value) {
32
+ if (!isAuthorizationKeyThumbprint(value)) {
33
+ throw new AuthorizationKeyTransitionError('invalid_key_thumbprint');
34
+ }
35
+ }
36
+ export class AuthorizationKeyTransitionError extends Error {
37
+ code;
38
+ constructor(code) {
39
+ super(code);
40
+ this.name = 'AuthorizationKeyTransitionError';
41
+ this.code = code;
42
+ }
43
+ }
44
+ //# sourceMappingURL=key-transition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-transition.js","sourceRoot":"","sources":["../../../src/identity/authorization/key-transition.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,kCAAkC,GAAG,4BAAqC,CAAC;AAaxF,kFAAkF;AAClF,MAAM,UAAU,0BAA0B,CACxC,KAAsC;IAEtC,mBAAmB,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAClD,IAAI,KAAK,CAAC,oBAAoB,KAAK,IAAI;QAAE,mBAAmB,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAEzF,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnB,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC;QAC1D,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,KAAK,kCAAkC,EAAE,CAAC;YAClE,MAAM,IAAI,+BAA+B,CAAC,0BAA0B,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,KAAK,CAAC,sBAAsB,KAAK,KAAK,CAAC,QAAQ,CAAC,qBAAqB,EAAE,CAAC;YAC1E,MAAM,IAAI,+BAA+B,CAAC,wBAAwB,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,KAAK,CAAC,oBAAoB,KAAK,KAAK,CAAC,sBAAsB;YAAE,OAAO,WAAW,CAAC;QACpF,IAAI,KAAK,CAAC,oBAAoB,KAAK,KAAK,CAAC,QAAQ,CAAC,qBAAqB,EAAE,CAAC;YACxE,MAAM,IAAI,+BAA+B,CAAC,yBAAyB,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,oBAAoB,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACtD,IAAI,KAAK,CAAC,oBAAoB,KAAK,KAAK,CAAC,sBAAsB;QAAE,OAAO,WAAW,CAAC;IACpF,MAAM,IAAI,+BAA+B,CAAC,cAAc,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,KAAc;IACzD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAc;IACzC,IAAI,CAAC,4BAA4B,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,+BAA+B,CAAC,wBAAwB,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AASD,MAAM,OAAO,+BAAgC,SAAQ,KAAK;IAC/C,IAAI,CAAsC;IAEnD,YAAY,IAAyC;QACnD,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,CAAC,IAAI,GAAG,iCAAiC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
@@ -20,6 +20,9 @@ needs.
20
20
  single-flights concurrent renewal, and performs no silent auth fallback.
21
21
  5. Audiences and scopes are closed sets. Adding a consumer changes this module
22
22
  and its tests before any issuer or verifier accepts it.
23
+ 6. A machine key is pinned once. Replacement requires an explicit, browser-
24
+ approved recovery bound to the expected old thumbprint and a different
25
+ candidate key; retrying an already completed replacement is idempotent.
23
26
 
24
27
  ## Public surface
25
28
 
@@ -28,6 +31,7 @@ Import `@amalgm/core/authorization` for:
28
31
  - `AUTHORIZATION_POLICY` and the audience/scope types;
29
32
  - access-token and DPoP claim contracts;
30
33
  - URL and method normalization;
34
+ - the closed initial-pin, ordinary-reuse, and explicit-recovery transition;
31
35
  - `CredentialBroker`, whose issuer and signer effects are injected by Shell.
32
36
 
33
37
  The default access lifetime is 15 minutes. Cached credentials refresh at least
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "The Amalgm kernel SDK — identity, transport, supervision, and host capability contracts behind host-injected adapters.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -67,8 +67,7 @@
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsc -p tsconfig.json",
70
- "test": "npm run build && node --test tests/",
71
- "test:parity": "npm run build && node --test tests/parity/",
70
+ "test": "npm run build && node --test",
72
71
  "examine:machine-setup": "npm run build && node --test tests/supervision/installation-plan.test.js tests/machine-setup/examination.test.js",
73
72
  "examine:user-ground": "npm run build && npm --prefix ../amalgm-live run build && node --test examinations/user-ground/login.examination.mjs"
74
73
  },