@amalgm/core 0.1.0
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 +94 -0
- package/README.md +30 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/identity/auth-records.d.ts +147 -0
- package/dist/identity/auth-records.d.ts.map +1 -0
- package/dist/identity/auth-records.js +354 -0
- package/dist/identity/auth-records.js.map +1 -0
- package/dist/identity/ids.d.ts +57 -0
- package/dist/identity/ids.d.ts.map +1 -0
- package/dist/identity/ids.js +80 -0
- package/dist/identity/ids.js.map +1 -0
- package/dist/identity/index.d.ts +14 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +14 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/labels.d.ts +115 -0
- package/dist/identity/labels.d.ts.map +1 -0
- package/dist/identity/labels.js +154 -0
- package/dist/identity/labels.js.map +1 -0
- package/dist/identity/layout.d.ts +188 -0
- package/dist/identity/layout.d.ts.map +1 -0
- package/dist/identity/layout.js +233 -0
- package/dist/identity/layout.js.map +1 -0
- package/dist/identity/ports.d.ts +31 -0
- package/dist/identity/ports.d.ts.map +1 -0
- package/dist/identity/ports.js +9 -0
- package/dist/identity/ports.js.map +1 -0
- package/dist/identity/product-state.d.ts +49 -0
- package/dist/identity/product-state.d.ts.map +1 -0
- package/dist/identity/product-state.js +55 -0
- package/dist/identity/product-state.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/supervision/app-health.d.ts +148 -0
- package/dist/supervision/app-health.d.ts.map +1 -0
- package/dist/supervision/app-health.js +165 -0
- package/dist/supervision/app-health.js.map +1 -0
- package/dist/supervision/app-restart.d.ts +105 -0
- package/dist/supervision/app-restart.d.ts.map +1 -0
- package/dist/supervision/app-restart.js +97 -0
- package/dist/supervision/app-restart.js.map +1 -0
- package/dist/supervision/health.d.ts +218 -0
- package/dist/supervision/health.d.ts.map +1 -0
- package/dist/supervision/health.js +227 -0
- package/dist/supervision/health.js.map +1 -0
- package/dist/supervision/index.d.ts +17 -0
- package/dist/supervision/index.d.ts.map +1 -0
- package/dist/supervision/index.js +17 -0
- package/dist/supervision/index.js.map +1 -0
- package/dist/supervision/service-registration.d.ts +343 -0
- package/dist/supervision/service-registration.d.ts.map +1 -0
- package/dist/supervision/service-registration.js +543 -0
- package/dist/supervision/service-registration.js.map +1 -0
- package/dist/supervision/update.d.ts +231 -0
- package/dist/supervision/update.d.ts.map +1 -0
- package/dist/supervision/update.js +336 -0
- package/dist/supervision/update.js.map +1 -0
- package/dist/transport/admission.d.ts +235 -0
- package/dist/transport/admission.d.ts.map +1 -0
- package/dist/transport/admission.js +313 -0
- package/dist/transport/admission.js.map +1 -0
- package/dist/transport/auth.d.ts +232 -0
- package/dist/transport/auth.d.ts.map +1 -0
- package/dist/transport/auth.js +266 -0
- package/dist/transport/auth.js.map +1 -0
- package/dist/transport/index.d.ts +21 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +21 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/ingress.d.ts +94 -0
- package/dist/transport/ingress.d.ts.map +1 -0
- package/dist/transport/ingress.js +171 -0
- package/dist/transport/ingress.js.map +1 -0
- package/dist/transport/routing.d.ts +235 -0
- package/dist/transport/routing.d.ts.map +1 -0
- package/dist/transport/routing.js +288 -0
- package/dist/transport/routing.js.map +1 -0
- package/dist/transport/shipped-routes.d.ts +21 -0
- package/dist/transport/shipped-routes.d.ts.map +1 -0
- package/dist/transport/shipped-routes.js +53 -0
- package/dist/transport/shipped-routes.js.map +1 -0
- package/package.json +51 -0
package/PURPOSE.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @amalgm/core
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`@amalgm/core` is the Amalgm kernel: the smallest set of contracts and
|
|
6
|
+
decision logic a machine needs to **boot** the platform, **prove who it is**,
|
|
7
|
+
**stay alive**, and **route traffic to everything else**. Every product —
|
|
8
|
+
live, chat, automations, apps, agents, toolbox, channels — rides on core;
|
|
9
|
+
core depends on none of them.
|
|
10
|
+
|
|
11
|
+
The membership test for this repository is one sentence: **if it is needed to
|
|
12
|
+
boot, supervise, or route to an app, and it cannot itself be an app, it is
|
|
13
|
+
core. Everything else is not.**
|
|
14
|
+
|
|
15
|
+
Core owns exactly four things:
|
|
16
|
+
|
|
17
|
+
1. **Identity** — who this machine is, for which user, on which branch.
|
|
18
|
+
The `~/.amalgm/<user>/<branch>/` layout, machine/instance identity,
|
|
19
|
+
auth-store contracts, and the branch-binding rule surfaces use to decide
|
|
20
|
+
whether they may attach.
|
|
21
|
+
2. **Transport** — the pipe. The local gateway's routing and admission
|
|
22
|
+
rules, the tunnel connection contract, and the event rail. The pipe
|
|
23
|
+
carries payloads it never interprets: sync semantics belong to
|
|
24
|
+
`@amalgm/live`, chat semantics to `@amalgm/chat`. (SIM card, not
|
|
25
|
+
FaceTime.)
|
|
26
|
+
3. **Supervision** — the cascade that keeps everything running: launchd →
|
|
27
|
+
CLI parent → runtime → app supervisor, health contracts, restart policy,
|
|
28
|
+
and the updater.
|
|
29
|
+
4. **Host capabilities** — the narrow, named powers a host grants
|
|
30
|
+
(spawn a service, write a plist, open a port), expressed as adapter
|
|
31
|
+
ports, never reached for directly.
|
|
32
|
+
|
|
33
|
+
`docs/responsibilities.md` enumerates every responsibility in full and maps
|
|
34
|
+
each engine module to its target repo. This repository is where the kernel's
|
|
35
|
+
rules become executable and testable; the engine repo remains the production
|
|
36
|
+
source until each module is cut over.
|
|
37
|
+
|
|
38
|
+
## Axioms
|
|
39
|
+
|
|
40
|
+
1. **Core is the floor, not the building.** Nothing product-shaped lives
|
|
41
|
+
here — no chat, no documents, no automations, no tools. A feature that
|
|
42
|
+
could be an app is a bug in core.
|
|
43
|
+
2. **Identity is one story.** User, machine, branch — resolved once, by one
|
|
44
|
+
rule set, and every path, socket, and token scope derives from it. A
|
|
45
|
+
second place that computes a home directory or a branch match is a bug.
|
|
46
|
+
3. **The pipe never reads the mail.** Transport moves opaque payloads
|
|
47
|
+
between named endpoints. A routing rule that inspects payload semantics
|
|
48
|
+
belongs to the product that owns the payload.
|
|
49
|
+
4. **One routing table.** Gateway, tunnel, and runtime consult one
|
|
50
|
+
definition of prefixes, ports, and admission. The engine's three drifted
|
|
51
|
+
copies are the wound this axiom closes.
|
|
52
|
+
5. **Supervision flows one way.** launchd supervises the parent, the parent
|
|
53
|
+
supervises the runtime, the runtime supervises apps. A child never
|
|
54
|
+
restarts its parent, and each layer reports its children's health
|
|
55
|
+
honestly — `degraded` and `unresponsive` are states, not failures to
|
|
56
|
+
hide.
|
|
57
|
+
6. **A host supplies its own effects.** The SDK receives process,
|
|
58
|
+
filesystem, clock, network, and crypto adapters. Nothing in `src/` may
|
|
59
|
+
import `node:fs`, `node:net`, `node:child_process`, or read an
|
|
60
|
+
environment variable.
|
|
61
|
+
7. **Protocol evolution is additive only.** Every peer treats missing
|
|
62
|
+
fields and unknown frames as "the other end is older" and keeps the
|
|
63
|
+
legacy behavior. A change that breaks a deployed peer is rejected in
|
|
64
|
+
review.
|
|
65
|
+
8. **Contracts are byte-exact.** Path formats, plist labels, health
|
|
66
|
+
vocabularies, error strings, and version-compare rules are preserved
|
|
67
|
+
verbatim from the shipped engine; parity tests pin them until cutover.
|
|
68
|
+
9. **The current runtime remains the single production source until moved.**
|
|
69
|
+
This package extracts contracts and decision logic with their tests; it
|
|
70
|
+
does not create a parallel execution path. The engine adopts it module by
|
|
71
|
+
module, deleting its duplicated copy in the same change.
|
|
72
|
+
10. **Amalgm's own surfaces use this SDK.** A private shortcut in the CLI,
|
|
73
|
+
gateway, or runtime means the SDK is incomplete — fix the SDK, not the
|
|
74
|
+
caller.
|
|
75
|
+
|
|
76
|
+
## Five surfaces, one behavior
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
src/ the SDK — the behavior. Contracts + decision cores + ports.
|
|
80
|
+
surfaces/api REST router factory a host mounts (identity/health/routing).
|
|
81
|
+
surfaces/cli `amalgm-core` — resolve layouts, explain routes, check health.
|
|
82
|
+
surfaces/mcp MCP server exposing the same actions to agents.
|
|
83
|
+
skills/core the skill: teaches an agent the kernel via the CLI/MCP.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Every surface is a thin adapter over `src/`. A surface that implements
|
|
87
|
+
behavior instead of delegating to the SDK is a bug.
|
|
88
|
+
|
|
89
|
+
## Status
|
|
90
|
+
|
|
91
|
+
Charter stage. Inventory of the engine's kernel territory is in
|
|
92
|
+
`docs/responsibilities.md`; extraction proceeds pure-cores-first, exactly as
|
|
93
|
+
`@amalgm/live` was built. The engine runtime and CLI remain the production
|
|
94
|
+
source; nothing consumes this package yet.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @amalgm/core
|
|
2
|
+
|
|
3
|
+
The Amalgm kernel SDK: identity, transport, and supervision contracts —
|
|
4
|
+
every law a machine needs to boot the platform, prove who it is, stay
|
|
5
|
+
alive, and route traffic to everything else. Start with
|
|
6
|
+
[PURPOSE.md](PURPOSE.md) (the charter) and
|
|
7
|
+
[docs/responsibilities.md](docs/responsibilities.md) (every duty, and the
|
|
8
|
+
knife table mapping the engine monolith onto its target repos).
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/identity/ labels, layout grammar, ids, auth records
|
|
12
|
+
src/transport/ the auth boundary, ONE routing table, admission, ingress
|
|
13
|
+
src/supervision/ health contracts, restart machines, updater laws,
|
|
14
|
+
service registration
|
|
15
|
+
src/adapters/ the ports a host injects (clock, hash, random, hmac…)
|
|
16
|
+
surfaces/ cli · api · mcp — thin doors over src/
|
|
17
|
+
skills/core/ the skill that teaches an agent this kernel
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
npm test # build + every exam
|
|
23
|
+
node surfaces/cli/amalgm-core.mjs label canary
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Rules of the house: zero runtime dependencies; nothing in `src/` imports
|
|
27
|
+
`node:*` or reads `process.env`; contracts are byte-exact against the
|
|
28
|
+
shipped engine (the production source until cutover — PURPOSE.md axiom 9);
|
|
29
|
+
tests are exams — each states a law, and a mutant must be convicted by at
|
|
30
|
+
least one.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter ports — every real-world effect the kernel SDK needs,
|
|
3
|
+
* gathered in one place so a host can see the full contract it must
|
|
4
|
+
* satisfy (PURPOSE.md axiom 6: a host supplies its own effects).
|
|
5
|
+
*
|
|
6
|
+
* Nothing in `src/` reaches for `node:*` or `process.env`; these types are
|
|
7
|
+
* the entire boundary. A Node host wires them from `node:crypto`,
|
|
8
|
+
* `node:path`, and `Date`; a test wires them from fixtures.
|
|
9
|
+
*/
|
|
10
|
+
export type { ClockIsoPort, ClockMsPort, RandomHexPort, Sha256HexPort, ResolvePathPort, } from '../identity/ports.js';
|
|
11
|
+
export type { TimingSafeEqualPort } from '../transport/auth.js';
|
|
12
|
+
export type { HmacHexPort } from '../transport/ingress.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter ports — every real-world effect the kernel SDK needs,
|
|
3
|
+
* gathered in one place so a host can see the full contract it must
|
|
4
|
+
* satisfy (PURPOSE.md axiom 6: a host supplies its own effects).
|
|
5
|
+
*
|
|
6
|
+
* Nothing in `src/` reaches for `node:*` or `process.env`; these types are
|
|
7
|
+
* the entire boundary. A Node host wires them from `node:crypto`,
|
|
8
|
+
* `node:path`, and `Date`; a test wires them from fixtures.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential-record shaping laws (engine packages/amalgm/lib/auth-store.js
|
|
3
|
+
* and runtime/scripts/proxy-token-store.js) — how a computer record splits
|
|
4
|
+
* into its PUBLIC face (`computer.json`) and its SECRET envelope
|
|
5
|
+
* (`auth.json`), how the two merge back, and when a proxy token counts as
|
|
6
|
+
* fresh. Pure record computations only: reading, writing, and env access
|
|
7
|
+
* stay host-side; the clock arrives as a port.
|
|
8
|
+
*
|
|
9
|
+
* Records are loose JSON from disk, so inputs are typed as they truly are:
|
|
10
|
+
* dictionaries of unknowns, validated key by key.
|
|
11
|
+
*/
|
|
12
|
+
import type { ClockIsoPort, ClockMsPort } from './ports.js';
|
|
13
|
+
type Dict = Record<string, unknown>;
|
|
14
|
+
/** Emails compare lowercased; a record's email is stored lowercased. */
|
|
15
|
+
export declare function normalizeUserEmail(value: unknown): string;
|
|
16
|
+
/** A record's email under any of its historical spellings (auth-store.js:74-76). */
|
|
17
|
+
export declare function userEmailFromRecord(record: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* Every key that must NEVER appear in the public `computer.json`
|
|
20
|
+
* (auth-store.js:24-35). Secrets live only in `auth.json`; the exact list —
|
|
21
|
+
* including the legacy camelCase and hmac spellings — is the contract.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SECRET_COMPUTER_KEYS: ReadonlySet<string>;
|
|
24
|
+
/** Does this record carry any secret worth an auth envelope?
|
|
25
|
+
* (auth-store.js:78-81). Whitespace-only values do not count. */
|
|
26
|
+
export declare function hasComputerSecrets(record: unknown): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The public face of a computer record (auth-store.js:83-92): the record
|
|
29
|
+
* with every secret key removed, its email normalized, and an `auth_file`
|
|
30
|
+
* pointer naming the sibling envelope by BASENAME — the record must stay
|
|
31
|
+
* true when the directory moves. DELIBERATE DEVIATION: the engine's
|
|
32
|
+
* parameter is a full path passed through `node:path.basename`; the same
|
|
33
|
+
* basename law is implemented here inline to stay dependency-free.
|
|
34
|
+
*/
|
|
35
|
+
export declare function publicComputerRecord(record: unknown, authFile?: string): Dict;
|
|
36
|
+
/**
|
|
37
|
+
* The full secret envelope distilled from a computer record
|
|
38
|
+
* (auth-store.js:94-156):
|
|
39
|
+
*
|
|
40
|
+
* { schema_version: 1, computer_id, device_id, user_id, user_email,
|
|
41
|
+
* app_url, tunnel: { token, url, chat_tunnel_url }, machine: { token },
|
|
42
|
+
* proxy: { token, expires_in, expires_at, refreshed_at } }
|
|
43
|
+
*
|
|
44
|
+
* Merging over `existing` is additive: identity keys are copied only when
|
|
45
|
+
* the record actually carries them, each token section is (re)stamped only
|
|
46
|
+
* when its token arrives, and a record carrying only tunnel URLs updates
|
|
47
|
+
* the URLs while PRESERVING the existing tunnel token. `expires_in`
|
|
48
|
+
* collapses to undefined (dropped by JSON) when absent or zero. Legacy
|
|
49
|
+
* proxy-token spellings (proxyToken, hmac_token, hmacToken) are honored on
|
|
50
|
+
* input; the envelope speaks only the canonical shape.
|
|
51
|
+
*/
|
|
52
|
+
export declare function authFromRecord(record: unknown, existing: unknown, ports: ClockIsoPort): Dict;
|
|
53
|
+
/**
|
|
54
|
+
* Rejoin a public record with its secret envelope (auth-store.js:158-189).
|
|
55
|
+
* THE REFUSAL: when both sides name a computer_id and they disagree, the
|
|
56
|
+
* envelope is IGNORED wholesale — credentials from one machine identity
|
|
57
|
+
* must never dress up a record claiming another. On agreement the envelope
|
|
58
|
+
* only fills gaps; a value the record already carries always wins. Absent
|
|
59
|
+
* envelope values fill gaps as `''` — exactly the engine's bytes on disk.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mergeRecordAuth(record: unknown, auth: unknown): Dict | null;
|
|
62
|
+
/**
|
|
63
|
+
* May this stored record serve the runtime labeled `currentLabel`?
|
|
64
|
+
* (auth-store.js:191-199). The record's claim is read with key precedence
|
|
65
|
+
* runtime_label > label > branch through the NON-coercing normalizer, so a
|
|
66
|
+
* record with no recognizable label — one that predates labels — belongs
|
|
67
|
+
* to `main` and only main may claim it. Pure over the injected current
|
|
68
|
+
* label instead of module state.
|
|
69
|
+
*/
|
|
70
|
+
export declare function recordMatchesCurrentRuntime(record: unknown, currentLabel: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The scope guard (auth-store.js:201-209): credentials for user A must
|
|
73
|
+
* never be saved into user B's scope. Enforced only when both sides are
|
|
74
|
+
* known and the scope is a real user — the `local` scope predates
|
|
75
|
+
* enrollment and accepts anyone. The refusal string is the ratified text.
|
|
76
|
+
*/
|
|
77
|
+
export declare function assertRecordMatchesUserScope(record: unknown, context: {
|
|
78
|
+
readonly runtimeLabel: string;
|
|
79
|
+
readonly userScope: string;
|
|
80
|
+
}): void;
|
|
81
|
+
/** Refresh proxy tokens 10 minutes before they die (auth-store.js:22,
|
|
82
|
+
* proxy-token-store.js:20) — one shared buffer, so the two freshness laws
|
|
83
|
+
* below can never disagree about "soon". */
|
|
84
|
+
export declare const PROXY_REFRESH_BUFFER_MS: number;
|
|
85
|
+
/**
|
|
86
|
+
* A record's proxy token under every spelling (auth-store.js:298-306), with
|
|
87
|
+
* an explicitly supplied environment override winning. Pure: the engine
|
|
88
|
+
* reads `process.env.AMALGM_PROXY_TOKEN`; the host passes that value in.
|
|
89
|
+
*/
|
|
90
|
+
export declare function proxyTokenFromRecord(record: unknown, envProxyToken?: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* The RECORD-side freshness law (auth-store.js:308-313), asked of a flat
|
|
93
|
+
* computer record: no token at all means refresh (true); a token whose
|
|
94
|
+
* expiry is missing or unparseable is trusted (false) — a legacy token
|
|
95
|
+
* without metadata keeps working; otherwise refresh inside the buffer.
|
|
96
|
+
* Note the deliberate asymmetry with {@link proxyTokenExpiresSoon}: over
|
|
97
|
+
* there, missing metadata means refresh — the envelope is expected to
|
|
98
|
+
* carry it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function proxyTokenNeedsRefresh(record: unknown, ports: ClockMsPort, envProxyToken?: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The ENVELOPE-side freshness law (proxy-token-store.js:110-117), asked of
|
|
103
|
+
* an auth envelope, three branches exactly: no token → false (nothing to
|
|
104
|
+
* refresh ahead of); token with missing OR unparseable expiry → true (an
|
|
105
|
+
* envelope is expected to know its expiry; not knowing is stale); else
|
|
106
|
+
* refresh inside the buffer.
|
|
107
|
+
*/
|
|
108
|
+
export declare function proxyTokenExpiresSoon(auth: unknown, ports: ClockMsPort): boolean;
|
|
109
|
+
/** Everything a proxy-token refresh call needs, distilled from the pair. */
|
|
110
|
+
export interface ProxyRefreshContext {
|
|
111
|
+
readonly machineToken: string;
|
|
112
|
+
readonly appUrl: string;
|
|
113
|
+
readonly computerId: string;
|
|
114
|
+
readonly deviceId: string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The refresh-credential precedence chain (proxy-token-store.js:88-98):
|
|
118
|
+
* the machine token is the machine's own auth token first, the legacy flat
|
|
119
|
+
* spelling second, and — last resort — the tunnel token; addresses and ids
|
|
120
|
+
* prefer the record over the envelope.
|
|
121
|
+
*/
|
|
122
|
+
export declare function proxyRefreshContext(record: unknown, auth: unknown): ProxyRefreshContext;
|
|
123
|
+
/**
|
|
124
|
+
* May a refresh even be attempted? (proxy-token-store.js:100-103): a
|
|
125
|
+
* machine token, an app URL, and a computer id are required; the device id
|
|
126
|
+
* is optional context.
|
|
127
|
+
*/
|
|
128
|
+
export declare function canRefreshProxyToken(context: {
|
|
129
|
+
readonly record: unknown;
|
|
130
|
+
readonly auth: unknown;
|
|
131
|
+
}): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* The persisted `auth.json` after a successful proxy-token refresh
|
|
134
|
+
* (proxy-token-store.js:154-173): the prior envelope carried forward,
|
|
135
|
+
* identity restated from the refresh context, the server's answers winning
|
|
136
|
+
* over prior values field by field, and both `proxy.refreshed_at` and
|
|
137
|
+
* `updated_at` stamped now. `expires_in` collapses to undefined (dropped
|
|
138
|
+
* by JSON) when absent or zero. The caller guards with
|
|
139
|
+
* {@link canRefreshProxyToken}; this is only the shape law.
|
|
140
|
+
*/
|
|
141
|
+
export declare function refreshedProxyAuth(input: {
|
|
142
|
+
readonly record: unknown;
|
|
143
|
+
readonly auth: unknown;
|
|
144
|
+
readonly refreshed: unknown;
|
|
145
|
+
}, ports: ClockIsoPort): Dict;
|
|
146
|
+
export {};
|
|
147
|
+
//# sourceMappingURL=auth-records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-records.d.ts","sourceRoot":"","sources":["../../src/identity/auth-records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpC,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAG3D;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAWnD,CAAC;AAEH;iEACiE;AACjE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAI3D;AAUD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,SAAc,GAAG,IAAI,CASlF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,YAAY,GAClB,IAAI,CA+DN;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAoC3E;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAK1F;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,OAAO,EACf,OAAO,EAAE;IAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACrE,IAAI,CAQN;AAED;;4CAE4C;AAC5C,eAAO,MAAM,uBAAuB,QAAiB,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,SAAK,GAAG,MAAM,CAShF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,WAAW,EAClB,aAAa,SAAK,GACjB,OAAO,CAMT;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAQhF;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,mBAAmB,CAcvF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAG3G;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE,EACxF,KAAK,EAAE,YAAY,GAClB,IAAI,CA0BN"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential-record shaping laws (engine packages/amalgm/lib/auth-store.js
|
|
3
|
+
* and runtime/scripts/proxy-token-store.js) — how a computer record splits
|
|
4
|
+
* into its PUBLIC face (`computer.json`) and its SECRET envelope
|
|
5
|
+
* (`auth.json`), how the two merge back, and when a proxy token counts as
|
|
6
|
+
* fresh. Pure record computations only: reading, writing, and env access
|
|
7
|
+
* stay host-side; the clock arrives as a port.
|
|
8
|
+
*
|
|
9
|
+
* Records are loose JSON from disk, so inputs are typed as they truly are:
|
|
10
|
+
* dictionaries of unknowns, validated key by key.
|
|
11
|
+
*/
|
|
12
|
+
import { normalizeRuntimeLabel } from './labels.js';
|
|
13
|
+
const asDict = (value) => (value && typeof value === 'object' ? value : null);
|
|
14
|
+
function cleanString(value) {
|
|
15
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
16
|
+
}
|
|
17
|
+
/** Emails compare lowercased; a record's email is stored lowercased. */
|
|
18
|
+
export function normalizeUserEmail(value) {
|
|
19
|
+
return cleanString(value).toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
/** A record's email under any of its historical spellings (auth-store.js:74-76). */
|
|
22
|
+
export function userEmailFromRecord(record) {
|
|
23
|
+
const r = asDict(record);
|
|
24
|
+
return normalizeUserEmail(r?.['user_email'] || r?.['userEmail'] || r?.['email']);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Every key that must NEVER appear in the public `computer.json`
|
|
28
|
+
* (auth-store.js:24-35). Secrets live only in `auth.json`; the exact list —
|
|
29
|
+
* including the legacy camelCase and hmac spellings — is the contract.
|
|
30
|
+
*/
|
|
31
|
+
export const SECRET_COMPUTER_KEYS = new Set([
|
|
32
|
+
'tunnel_token',
|
|
33
|
+
'computer_auth_token',
|
|
34
|
+
'proxy_token',
|
|
35
|
+
'proxy_token_expires_in',
|
|
36
|
+
'proxy_token_expires_at',
|
|
37
|
+
'proxy_token_refreshed_at',
|
|
38
|
+
'proxyToken',
|
|
39
|
+
'proxyTokenExpiresAt',
|
|
40
|
+
'hmac_token',
|
|
41
|
+
'hmacToken',
|
|
42
|
+
]);
|
|
43
|
+
/** Does this record carry any secret worth an auth envelope?
|
|
44
|
+
* (auth-store.js:78-81). Whitespace-only values do not count. */
|
|
45
|
+
export function hasComputerSecrets(record) {
|
|
46
|
+
const r = asDict(record);
|
|
47
|
+
if (!r)
|
|
48
|
+
return false;
|
|
49
|
+
return Array.from(SECRET_COMPUTER_KEYS).some((key) => cleanString(r[key]));
|
|
50
|
+
}
|
|
51
|
+
/** The engine's `path.basename` over both separators, with trailing
|
|
52
|
+
* separators stripped — only the file's own name is published. */
|
|
53
|
+
const baseName = (file) => {
|
|
54
|
+
const trimmed = file.replace(/[\\/]+$/, '');
|
|
55
|
+
const cut = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
56
|
+
return cut === -1 ? trimmed : trimmed.slice(cut + 1);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The public face of a computer record (auth-store.js:83-92): the record
|
|
60
|
+
* with every secret key removed, its email normalized, and an `auth_file`
|
|
61
|
+
* pointer naming the sibling envelope by BASENAME — the record must stay
|
|
62
|
+
* true when the directory moves. DELIBERATE DEVIATION: the engine's
|
|
63
|
+
* parameter is a full path passed through `node:path.basename`; the same
|
|
64
|
+
* basename law is implemented here inline to stay dependency-free.
|
|
65
|
+
*/
|
|
66
|
+
export function publicComputerRecord(record, authFile = 'auth.json') {
|
|
67
|
+
const out = {
|
|
68
|
+
...(asDict(record) || {}),
|
|
69
|
+
auth_file: baseName(authFile),
|
|
70
|
+
};
|
|
71
|
+
const email = userEmailFromRecord(out);
|
|
72
|
+
if (email)
|
|
73
|
+
out['user_email'] = email;
|
|
74
|
+
for (const key of SECRET_COMPUTER_KEYS)
|
|
75
|
+
delete out[key];
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The full secret envelope distilled from a computer record
|
|
80
|
+
* (auth-store.js:94-156):
|
|
81
|
+
*
|
|
82
|
+
* { schema_version: 1, computer_id, device_id, user_id, user_email,
|
|
83
|
+
* app_url, tunnel: { token, url, chat_tunnel_url }, machine: { token },
|
|
84
|
+
* proxy: { token, expires_in, expires_at, refreshed_at } }
|
|
85
|
+
*
|
|
86
|
+
* Merging over `existing` is additive: identity keys are copied only when
|
|
87
|
+
* the record actually carries them, each token section is (re)stamped only
|
|
88
|
+
* when its token arrives, and a record carrying only tunnel URLs updates
|
|
89
|
+
* the URLs while PRESERVING the existing tunnel token. `expires_in`
|
|
90
|
+
* collapses to undefined (dropped by JSON) when absent or zero. Legacy
|
|
91
|
+
* proxy-token spellings (proxyToken, hmac_token, hmacToken) are honored on
|
|
92
|
+
* input; the envelope speaks only the canonical shape.
|
|
93
|
+
*/
|
|
94
|
+
export function authFromRecord(record, existing, ports) {
|
|
95
|
+
const now = ports.nowIso();
|
|
96
|
+
const r = asDict(record);
|
|
97
|
+
const auth = {
|
|
98
|
+
schema_version: 1,
|
|
99
|
+
...(asDict(existing) || {}),
|
|
100
|
+
updated_at: now,
|
|
101
|
+
};
|
|
102
|
+
for (const key of ['computer_id', 'device_id', 'user_id', 'user_email', 'app_url']) {
|
|
103
|
+
const value = key === 'user_email' ? userEmailFromRecord(r) : cleanString(r?.[key]);
|
|
104
|
+
if (value)
|
|
105
|
+
auth[key] = value;
|
|
106
|
+
}
|
|
107
|
+
const priorTunnel = asDict(auth['tunnel']);
|
|
108
|
+
const tunnelToken = cleanString(r?.['tunnel_token']);
|
|
109
|
+
if (tunnelToken) {
|
|
110
|
+
auth['tunnel'] = {
|
|
111
|
+
...(priorTunnel || {}),
|
|
112
|
+
token: tunnelToken,
|
|
113
|
+
url: cleanString(r?.['tunnel_url']) || cleanString(priorTunnel?.['url']),
|
|
114
|
+
chat_tunnel_url: cleanString(r?.['chat_tunnel_url']) || cleanString(priorTunnel?.['chat_tunnel_url']),
|
|
115
|
+
updated_at: now,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
else if (r?.['tunnel_url'] || r?.['chat_tunnel_url']) {
|
|
119
|
+
auth['tunnel'] = {
|
|
120
|
+
...(priorTunnel || {}),
|
|
121
|
+
url: cleanString(r?.['tunnel_url']) || cleanString(priorTunnel?.['url']),
|
|
122
|
+
chat_tunnel_url: cleanString(r?.['chat_tunnel_url']) || cleanString(priorTunnel?.['chat_tunnel_url']),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const computerAuthToken = cleanString(r?.['computer_auth_token']);
|
|
126
|
+
if (computerAuthToken) {
|
|
127
|
+
auth['machine'] = {
|
|
128
|
+
...(asDict(auth['machine']) || {}),
|
|
129
|
+
token: computerAuthToken,
|
|
130
|
+
updated_at: now,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const proxyToken = (cleanString(r?.['proxy_token'])
|
|
134
|
+
|| cleanString(r?.['proxyToken'])
|
|
135
|
+
|| cleanString(r?.['hmac_token'])
|
|
136
|
+
|| cleanString(r?.['hmacToken']));
|
|
137
|
+
if (proxyToken) {
|
|
138
|
+
const priorProxy = asDict(auth['proxy']);
|
|
139
|
+
auth['proxy'] = {
|
|
140
|
+
...(priorProxy || {}),
|
|
141
|
+
token: proxyToken,
|
|
142
|
+
expires_in: Number(r?.['proxy_token_expires_in'] || priorProxy?.['expires_in'] || 0) || undefined,
|
|
143
|
+
expires_at: cleanString(r?.['proxy_token_expires_at'])
|
|
144
|
+
|| cleanString(r?.['proxyTokenExpiresAt'])
|
|
145
|
+
|| cleanString(priorProxy?.['expires_at']),
|
|
146
|
+
refreshed_at: cleanString(r?.['proxy_token_refreshed_at'])
|
|
147
|
+
|| cleanString(priorProxy?.['refreshed_at'])
|
|
148
|
+
|| now,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return auth;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Rejoin a public record with its secret envelope (auth-store.js:158-189).
|
|
155
|
+
* THE REFUSAL: when both sides name a computer_id and they disagree, the
|
|
156
|
+
* envelope is IGNORED wholesale — credentials from one machine identity
|
|
157
|
+
* must never dress up a record claiming another. On agreement the envelope
|
|
158
|
+
* only fills gaps; a value the record already carries always wins. Absent
|
|
159
|
+
* envelope values fill gaps as `''` — exactly the engine's bytes on disk.
|
|
160
|
+
*/
|
|
161
|
+
export function mergeRecordAuth(record, auth) {
|
|
162
|
+
const r = asDict(record);
|
|
163
|
+
if (!r)
|
|
164
|
+
return null;
|
|
165
|
+
const merged = { ...r };
|
|
166
|
+
const a = asDict(auth);
|
|
167
|
+
if (!a)
|
|
168
|
+
return merged;
|
|
169
|
+
const authComputerId = cleanString(a['computer_id']);
|
|
170
|
+
const recordComputerId = cleanString(r['computer_id']);
|
|
171
|
+
if (authComputerId && recordComputerId && authComputerId !== recordComputerId)
|
|
172
|
+
return merged;
|
|
173
|
+
const tunnel = asDict(a['tunnel']);
|
|
174
|
+
const machine = asDict(a['machine']);
|
|
175
|
+
const proxy = asDict(a['proxy']);
|
|
176
|
+
if (!merged['tunnel_token'])
|
|
177
|
+
merged['tunnel_token'] = cleanString(tunnel?.['token']) || cleanString(a['tunnel_token']);
|
|
178
|
+
if (!merged['tunnel_url'])
|
|
179
|
+
merged['tunnel_url'] = cleanString(tunnel?.['url']);
|
|
180
|
+
if (!merged['chat_tunnel_url'])
|
|
181
|
+
merged['chat_tunnel_url'] = cleanString(tunnel?.['chat_tunnel_url']);
|
|
182
|
+
if (!merged['computer_auth_token']) {
|
|
183
|
+
merged['computer_auth_token'] = cleanString(machine?.['token']) || cleanString(a['computer_auth_token']);
|
|
184
|
+
}
|
|
185
|
+
if (!merged['proxy_token']) {
|
|
186
|
+
merged['proxy_token'] = cleanString(proxy?.['token']) || cleanString(a['proxy_token']);
|
|
187
|
+
}
|
|
188
|
+
if (!merged['proxy_token_expires_at']) {
|
|
189
|
+
merged['proxy_token_expires_at'] = cleanString(proxy?.['expires_at']) || cleanString(a['proxy_token_expires_at']);
|
|
190
|
+
}
|
|
191
|
+
if (!merged['proxy_token_expires_in'] && proxy?.['expires_in']) {
|
|
192
|
+
merged['proxy_token_expires_in'] = proxy['expires_in'];
|
|
193
|
+
}
|
|
194
|
+
if (!merged['proxy_token_refreshed_at']) {
|
|
195
|
+
merged['proxy_token_refreshed_at'] = cleanString(proxy?.['refreshed_at']);
|
|
196
|
+
}
|
|
197
|
+
if (!merged['user_id'])
|
|
198
|
+
merged['user_id'] = cleanString(a['user_id']);
|
|
199
|
+
if (!merged['user_email'])
|
|
200
|
+
merged['user_email'] = normalizeUserEmail(a['user_email']);
|
|
201
|
+
if (!merged['app_url'])
|
|
202
|
+
merged['app_url'] = cleanString(a['app_url']);
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* May this stored record serve the runtime labeled `currentLabel`?
|
|
207
|
+
* (auth-store.js:191-199). The record's claim is read with key precedence
|
|
208
|
+
* runtime_label > label > branch through the NON-coercing normalizer, so a
|
|
209
|
+
* record with no recognizable label — one that predates labels — belongs
|
|
210
|
+
* to `main` and only main may claim it. Pure over the injected current
|
|
211
|
+
* label instead of module state.
|
|
212
|
+
*/
|
|
213
|
+
export function recordMatchesCurrentRuntime(record, currentLabel) {
|
|
214
|
+
const r = asDict(record);
|
|
215
|
+
const label = normalizeRuntimeLabel(r?.['runtime_label'] || r?.['label'] || r?.['branch']);
|
|
216
|
+
if (!label)
|
|
217
|
+
return currentLabel === 'main';
|
|
218
|
+
return label === currentLabel;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The scope guard (auth-store.js:201-209): credentials for user A must
|
|
222
|
+
* never be saved into user B's scope. Enforced only when both sides are
|
|
223
|
+
* known and the scope is a real user — the `local` scope predates
|
|
224
|
+
* enrollment and accepts anyone. The refusal string is the ratified text.
|
|
225
|
+
*/
|
|
226
|
+
export function assertRecordMatchesUserScope(record, context) {
|
|
227
|
+
const userId = cleanString(asDict(record)?.['user_id']);
|
|
228
|
+
const expectedScope = cleanString(context.userScope);
|
|
229
|
+
if (!userId || !expectedScope || expectedScope === 'local')
|
|
230
|
+
return;
|
|
231
|
+
if (userId === expectedScope)
|
|
232
|
+
return;
|
|
233
|
+
throw new Error(`Refusing to save ${context.runtimeLabel} computer credentials for user ${userId} in scope ${expectedScope}.`);
|
|
234
|
+
}
|
|
235
|
+
/** Refresh proxy tokens 10 minutes before they die (auth-store.js:22,
|
|
236
|
+
* proxy-token-store.js:20) — one shared buffer, so the two freshness laws
|
|
237
|
+
* below can never disagree about "soon". */
|
|
238
|
+
export const PROXY_REFRESH_BUFFER_MS = 10 * 60 * 1000;
|
|
239
|
+
/**
|
|
240
|
+
* A record's proxy token under every spelling (auth-store.js:298-306), with
|
|
241
|
+
* an explicitly supplied environment override winning. Pure: the engine
|
|
242
|
+
* reads `process.env.AMALGM_PROXY_TOKEN`; the host passes that value in.
|
|
243
|
+
*/
|
|
244
|
+
export function proxyTokenFromRecord(record, envProxyToken = '') {
|
|
245
|
+
const r = asDict(record);
|
|
246
|
+
return (cleanString(envProxyToken)
|
|
247
|
+
|| cleanString(r?.['proxy_token'])
|
|
248
|
+
|| cleanString(r?.['proxyToken'])
|
|
249
|
+
|| cleanString(r?.['hmac_token'])
|
|
250
|
+
|| cleanString(r?.['hmacToken']));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The RECORD-side freshness law (auth-store.js:308-313), asked of a flat
|
|
254
|
+
* computer record: no token at all means refresh (true); a token whose
|
|
255
|
+
* expiry is missing or unparseable is trusted (false) — a legacy token
|
|
256
|
+
* without metadata keeps working; otherwise refresh inside the buffer.
|
|
257
|
+
* Note the deliberate asymmetry with {@link proxyTokenExpiresSoon}: over
|
|
258
|
+
* there, missing metadata means refresh — the envelope is expected to
|
|
259
|
+
* carry it.
|
|
260
|
+
*/
|
|
261
|
+
export function proxyTokenNeedsRefresh(record, ports, envProxyToken = '') {
|
|
262
|
+
if (!proxyTokenFromRecord(record, envProxyToken))
|
|
263
|
+
return true;
|
|
264
|
+
const r = asDict(record);
|
|
265
|
+
const expiresAt = Date.parse(String(r?.['proxy_token_expires_at'] || r?.['proxyTokenExpiresAt'] || ''));
|
|
266
|
+
if (!Number.isFinite(expiresAt))
|
|
267
|
+
return false;
|
|
268
|
+
return expiresAt - ports.nowMs() < PROXY_REFRESH_BUFFER_MS;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The ENVELOPE-side freshness law (proxy-token-store.js:110-117), asked of
|
|
272
|
+
* an auth envelope, three branches exactly: no token → false (nothing to
|
|
273
|
+
* refresh ahead of); token with missing OR unparseable expiry → true (an
|
|
274
|
+
* envelope is expected to know its expiry; not knowing is stale); else
|
|
275
|
+
* refresh inside the buffer.
|
|
276
|
+
*/
|
|
277
|
+
export function proxyTokenExpiresSoon(auth, ports) {
|
|
278
|
+
const proxy = asDict(asDict(auth)?.['proxy']);
|
|
279
|
+
if (!cleanString(proxy?.['token']))
|
|
280
|
+
return false;
|
|
281
|
+
const expiresAtRaw = cleanString(proxy?.['expires_at']);
|
|
282
|
+
if (!expiresAtRaw)
|
|
283
|
+
return true;
|
|
284
|
+
const expiresAt = Date.parse(expiresAtRaw);
|
|
285
|
+
if (!Number.isFinite(expiresAt))
|
|
286
|
+
return true;
|
|
287
|
+
return expiresAt - ports.nowMs() < PROXY_REFRESH_BUFFER_MS;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The refresh-credential precedence chain (proxy-token-store.js:88-98):
|
|
291
|
+
* the machine token is the machine's own auth token first, the legacy flat
|
|
292
|
+
* spelling second, and — last resort — the tunnel token; addresses and ids
|
|
293
|
+
* prefer the record over the envelope.
|
|
294
|
+
*/
|
|
295
|
+
export function proxyRefreshContext(record, auth) {
|
|
296
|
+
const r = asDict(record);
|
|
297
|
+
const a = asDict(auth);
|
|
298
|
+
const machine = asDict(a?.['machine']);
|
|
299
|
+
const tunnel = asDict(a?.['tunnel']);
|
|
300
|
+
return {
|
|
301
|
+
machineToken: cleanString(machine?.['token'])
|
|
302
|
+
|| cleanString(a?.['computer_auth_token'])
|
|
303
|
+
|| cleanString(tunnel?.['token']),
|
|
304
|
+
appUrl: cleanString(r?.['app_url']) || cleanString(a?.['app_url']),
|
|
305
|
+
computerId: cleanString(r?.['computer_id']) || cleanString(a?.['computer_id']),
|
|
306
|
+
deviceId: cleanString(r?.['device_id']) || cleanString(a?.['device_id']),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* May a refresh even be attempted? (proxy-token-store.js:100-103): a
|
|
311
|
+
* machine token, an app URL, and a computer id are required; the device id
|
|
312
|
+
* is optional context.
|
|
313
|
+
*/
|
|
314
|
+
export function canRefreshProxyToken(context) {
|
|
315
|
+
const refresh = proxyRefreshContext(context.record, context.auth);
|
|
316
|
+
return !!(refresh.machineToken && refresh.appUrl && refresh.computerId);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The persisted `auth.json` after a successful proxy-token refresh
|
|
320
|
+
* (proxy-token-store.js:154-173): the prior envelope carried forward,
|
|
321
|
+
* identity restated from the refresh context, the server's answers winning
|
|
322
|
+
* over prior values field by field, and both `proxy.refreshed_at` and
|
|
323
|
+
* `updated_at` stamped now. `expires_in` collapses to undefined (dropped
|
|
324
|
+
* by JSON) when absent or zero. The caller guards with
|
|
325
|
+
* {@link canRefreshProxyToken}; this is only the shape law.
|
|
326
|
+
*/
|
|
327
|
+
export function refreshedProxyAuth(input, ports) {
|
|
328
|
+
const { appUrl, computerId, deviceId } = proxyRefreshContext(input.record, input.auth);
|
|
329
|
+
const r = asDict(input.record);
|
|
330
|
+
const a = asDict(input.auth) || {};
|
|
331
|
+
const refreshed = asDict(input.refreshed);
|
|
332
|
+
const priorProxy = asDict(a['proxy']);
|
|
333
|
+
const now = ports.nowIso();
|
|
334
|
+
return {
|
|
335
|
+
...a,
|
|
336
|
+
schema_version: 1,
|
|
337
|
+
computer_id: computerId,
|
|
338
|
+
device_id: deviceId,
|
|
339
|
+
user_id: cleanString(r?.['user_id']) || cleanString(a['user_id']),
|
|
340
|
+
user_email: cleanString(refreshed?.['user_email']).toLowerCase()
|
|
341
|
+
|| cleanString(r?.['user_email']).toLowerCase()
|
|
342
|
+
|| cleanString(a['user_email']).toLowerCase(),
|
|
343
|
+
app_url: appUrl,
|
|
344
|
+
proxy: {
|
|
345
|
+
...(priorProxy || {}),
|
|
346
|
+
token: cleanString(refreshed?.['proxy_token']) || cleanString(priorProxy?.['token']),
|
|
347
|
+
expires_in: Number(refreshed?.['proxy_token_expires_in'] || priorProxy?.['expires_in'] || 0) || undefined,
|
|
348
|
+
expires_at: cleanString(refreshed?.['proxy_token_expires_at']) || cleanString(priorProxy?.['expires_at']),
|
|
349
|
+
refreshed_at: now,
|
|
350
|
+
},
|
|
351
|
+
updated_at: now,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=auth-records.js.map
|