@jini-ai/core 0.3.1 → 0.4.1
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/CHANGELOG.md +136 -0
- package/LICENSE +201 -0
- package/README.md +154 -14
- package/dist/agent-tools.d.ts +29 -0
- package/dist/agent-tools.js +2 -0
- package/dist/api-token-auth.d.ts +12 -5
- package/dist/api-token-auth.js +7 -7
- package/dist/bindings.d.ts +16 -5
- package/dist/bindings.js +7 -6
- package/dist/{internal.d.ts → composition.d.ts} +9 -9
- package/dist/{internal.js → composition.js} +9 -9
- package/dist/contribution-registry.d.ts +16 -0
- package/dist/contribution-registry.js +12 -0
- package/dist/daemon.d.ts +12 -7
- package/dist/daemon.js +21 -9
- package/dist/gated-mutations/actor-identity.d.ts +20 -0
- package/dist/gated-mutations/actor-identity.js +31 -0
- package/dist/gated-mutations/gateway.d.ts +115 -0
- package/dist/gated-mutations/gateway.js +123 -0
- package/dist/gated-mutations/ports.d.ts +27 -0
- package/dist/gated-mutations/ports.js +2 -0
- package/dist/gated-mutations/token.d.ts +95 -0
- package/dist/gated-mutations/token.js +87 -0
- package/dist/gated-mutations.d.ts +5 -0
- package/dist/gated-mutations.js +5 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +37 -0
- package/dist/model-facing-tool-errors.js +71 -0
- package/dist/naming.d.ts +31 -0
- package/dist/naming.js +33 -0
- package/dist/origin-validation.d.ts +64 -19
- package/dist/origin-validation.js +43 -33
- package/dist/pack-lifecycle.d.ts +19 -12
- package/dist/pack-lifecycle.js +14 -14
- package/dist/pack.d.ts +41 -13
- package/dist/pack.js +2 -2
- package/dist/primitives/http.d.ts +57 -0
- package/dist/primitives/http.js +2 -0
- package/dist/primitives/ids.d.ts +8 -0
- package/dist/primitives/ids.js +5 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.js +8 -0
- package/dist/primitives/json.d.ts +10 -0
- package/dist/primitives/json.js +2 -0
- package/dist/primitives/logger.d.ts +26 -0
- package/dist/primitives/logger.js +17 -0
- package/dist/primitives/paths.d.ts +16 -0
- package/dist/primitives/paths.js +38 -0
- package/dist/primitives/result.d.ts +9 -0
- package/dist/primitives/result.js +2 -0
- package/dist/primitives/time.d.ts +17 -0
- package/dist/primitives/time.js +13 -0
- package/dist/redact.d.ts +43 -8
- package/dist/redact.js +121 -50
- package/dist/registration-kit.d.ts +357 -0
- package/dist/registration-kit.js +439 -0
- package/dist/text/index.d.ts +3 -0
- package/dist/text/index.js +2 -0
- package/dist/text/sanitize-untrusted-text.d.ts +22 -0
- package/dist/text/sanitize-untrusted-text.js +108 -0
- package/dist/token.d.ts +29 -2
- package/dist/token.js +22 -2
- package/dist/tool-registry.d.ts +32 -16
- package/dist/tool-registry.js +11 -11
- package/dist/tool-tokens.js +1 -1
- package/package.json +56 -11
- package/dist/api-token-auth.d.ts.map +0 -1
- package/dist/api-token-auth.js.map +0 -1
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js.map +0 -1
- package/dist/compose.typecheck.d.ts +0 -2
- package/dist/compose.typecheck.d.ts.map +0 -1
- package/dist/compose.typecheck.js +0 -29
- package/dist/compose.typecheck.js.map +0 -1
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/origin-validation.d.ts.map +0 -1
- package/dist/origin-validation.js.map +0 -1
- package/dist/pack-lifecycle.d.ts.map +0 -1
- package/dist/pack-lifecycle.js.map +0 -1
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -1
- package/dist/principal.d.ts.map +0 -1
- package/dist/principal.js.map +0 -1
- package/dist/redact.d.ts.map +0 -1
- package/dist/redact.js.map +0 -1
- package/dist/token.d.ts.map +0 -1
- package/dist/token.js.map +0 -1
- package/dist/tool-registry.d.ts.map +0 -1
- package/dist/tool-registry.js.map +0 -1
- package/dist/tool-tokens.d.ts.map +0 -1
- package/dist/tool-tokens.js.map +0 -1
package/dist/api-token-auth.js
CHANGED
|
@@ -4,27 +4,27 @@
|
|
|
4
4
|
* Whether a bearer-style API token gate should be enforced, and what the
|
|
5
5
|
* expected token value is. Generalized from an upstream flat daemon module:
|
|
6
6
|
* the origin hardcoded its host product's env-var names, now fields on
|
|
7
|
-
* {@link ApiTokenAuthEnvConfig} — see `
|
|
7
|
+
* {@link ApiTokenAuthEnvConfig} — see `archived provenance ledger` for the exact mapping.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
10
|
* Interprets `value` as a truthy flag: `'1'`, `'true'`, `'yes'`, or `'on'`
|
|
11
11
|
* (case-insensitive, trimmed). Anything else, including `undefined`, is
|
|
12
12
|
* falsy.
|
|
13
13
|
*/
|
|
14
|
-
export function isTruthyEnvFlag(value) {
|
|
14
|
+
export function isTruthyEnvFlag({ value }) {
|
|
15
15
|
const normalized = String(value || '').trim().toLowerCase();
|
|
16
16
|
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
17
17
|
}
|
|
18
18
|
/** Whether the API auth gate has been explicitly force-disabled. */
|
|
19
|
-
export function isApiAuthDisabled(config, env
|
|
20
|
-
return isTruthyEnvFlag(env[config.disableEnvVar]);
|
|
19
|
+
export function isApiAuthDisabled({ config, env }) {
|
|
20
|
+
return isTruthyEnvFlag({ value: env[config.disableEnvVar] });
|
|
21
21
|
}
|
|
22
22
|
/** The configured expected token value, trimmed; `''` when unset. */
|
|
23
|
-
export function apiTokenFromEnv(config, env
|
|
23
|
+
export function apiTokenFromEnv({ config, env }) {
|
|
24
24
|
return (env[config.tokenEnvVar] ?? '').trim();
|
|
25
25
|
}
|
|
26
26
|
/** Whether the token gate should actually run: a token is configured and it hasn't been disabled. */
|
|
27
|
-
export function isApiTokenMiddlewareEnabled(config, env
|
|
28
|
-
return apiTokenFromEnv(config, env).length > 0 && !isApiAuthDisabled(config, env);
|
|
27
|
+
export function isApiTokenMiddlewareEnabled({ config, env }) {
|
|
28
|
+
return apiTokenFromEnv({ config, env }).length > 0 && !isApiAuthDisabled({ config, env });
|
|
29
29
|
}
|
|
30
30
|
//# sourceMappingURL=api-token-auth.js.map
|
package/dist/bindings.d.ts
CHANGED
|
@@ -5,14 +5,25 @@ import type { ManyToken, Token } from './token.js';
|
|
|
5
5
|
* `Exclude<RequiredTokenIds<Packs>, BoundIds>` at compile time (see daemon.ts).
|
|
6
6
|
*/
|
|
7
7
|
export declare class Bindings<BoundIds extends string = never> {
|
|
8
|
+
constructor(_required: Record<string, never>);
|
|
8
9
|
private readonly one;
|
|
9
10
|
private readonly many;
|
|
10
|
-
bind<T, Id extends string>(
|
|
11
|
-
|
|
11
|
+
bind<T, Id extends string>({ token: t, impl }: {
|
|
12
|
+
token: Token<T, Id>;
|
|
13
|
+
impl: T;
|
|
14
|
+
}): Bindings<BoundIds | Id>;
|
|
15
|
+
bindMany<T, Id extends string>({ token: t, impl }: {
|
|
16
|
+
token: ManyToken<T, Id>;
|
|
17
|
+
impl: T;
|
|
18
|
+
}): Bindings<BoundIds | Id>;
|
|
12
19
|
/** @internal */
|
|
13
|
-
resolveOne<T>(
|
|
20
|
+
resolveOne<T>({ token: t }: {
|
|
21
|
+
token: Token<T, string>;
|
|
22
|
+
}): T;
|
|
14
23
|
/** @internal */
|
|
15
|
-
resolveMany<T>(
|
|
24
|
+
resolveMany<T>({ token: t }: {
|
|
25
|
+
token: ManyToken<T, string>;
|
|
26
|
+
}): T[];
|
|
16
27
|
}
|
|
17
|
-
export declare function bindings(): Bindings<never>;
|
|
28
|
+
export declare function bindings(required: Record<string, never>): Bindings<never>;
|
|
18
29
|
//# sourceMappingURL=bindings.d.ts.map
|
package/dist/bindings.js
CHANGED
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
* `Exclude<RequiredTokenIds<Packs>, BoundIds>` at compile time (see daemon.ts).
|
|
5
5
|
*/
|
|
6
6
|
export class Bindings {
|
|
7
|
+
constructor(_required) { }
|
|
7
8
|
one = new Map();
|
|
8
9
|
many = new Map();
|
|
9
|
-
bind(t, impl) {
|
|
10
|
+
bind({ token: t, impl }) {
|
|
10
11
|
if (this.one.has(t.id)) {
|
|
11
12
|
throw new Error(`duplicate binding: ${t.id} is already bound (a singleton token accepts exactly one binding)`);
|
|
12
13
|
}
|
|
13
14
|
this.one.set(t.id, { value: impl, version: t.version });
|
|
14
15
|
return this;
|
|
15
16
|
}
|
|
16
|
-
bindMany(t, impl) {
|
|
17
|
+
bindMany({ token: t, impl }) {
|
|
17
18
|
const list = this.many.get(t.id) ?? [];
|
|
18
19
|
list.push(impl);
|
|
19
20
|
this.many.set(t.id, list);
|
|
20
21
|
return this;
|
|
21
22
|
}
|
|
22
23
|
/** @internal */
|
|
23
|
-
resolveOne(t) {
|
|
24
|
+
resolveOne({ token: t }) {
|
|
24
25
|
const entry = this.one.get(t.id);
|
|
25
26
|
if (!entry) {
|
|
26
27
|
throw new Error(`missing binding: ${t.id}`);
|
|
@@ -31,11 +32,11 @@ export class Bindings {
|
|
|
31
32
|
return entry.value;
|
|
32
33
|
}
|
|
33
34
|
/** @internal */
|
|
34
|
-
resolveMany(t) {
|
|
35
|
+
resolveMany({ token: t }) {
|
|
35
36
|
return (this.many.get(t.id) ?? []);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
export function bindings() {
|
|
39
|
-
return new Bindings();
|
|
39
|
+
export function bindings(required) {
|
|
40
|
+
return new Bindings(required);
|
|
40
41
|
}
|
|
41
42
|
//# sourceMappingURL=bindings.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module @jini-ai/core/
|
|
2
|
+
* @module @jini-ai/core/composition
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Composition boundary for daemon assembly and authorized tool invocation.
|
|
5
|
+
* These capabilities stay off the root entry point; each has its own
|
|
6
|
+
* intended consumer:
|
|
7
7
|
*
|
|
8
8
|
* - {@link authorizeToolInvocation} so `@jini-ai/daemon`'s `ToolExecutor` can
|
|
9
9
|
* resolve a registered tool and run its authorization gate — the tool's
|
|
@@ -24,18 +24,18 @@
|
|
|
24
24
|
*
|
|
25
25
|
* **Correction (2026-07-19 hardening pass):** a package.json `exports` map
|
|
26
26
|
* subpath is NOT a language-level or per-consumer access modifier — Node's
|
|
27
|
-
* module resolution will happily resolve `@jini-ai/core/
|
|
27
|
+
* module resolution will happily resolve `@jini-ai/core/composition` for *any*
|
|
28
28
|
* package that depends on `@jini-ai/core`, not just the intended consumer.
|
|
29
29
|
* Earlier revisions of this doc comment claimed this boundary was
|
|
30
30
|
* "enforced" by the exports map; that was inaccurate and left the leak
|
|
31
31
|
* undetected. The actual enforcement is `scripts/check-engine-boundaries.ts`'s
|
|
32
|
-
* rule forbidding any *value* import from `@jini-ai/core/
|
|
32
|
+
* R6-internal-leak rule forbidding any *value* import from `@jini-ai/core/composition` outside
|
|
33
33
|
* `packages/daemon/**` (type-only imports of the DI-token-derivation types
|
|
34
|
-
* remain unrestricted, since they carry no runtime capability)
|
|
35
|
-
*
|
|
34
|
+
* remain unrestricted, since they carry no runtime capability). R6 checks this
|
|
35
|
+
* exact composition specifier as a second, independent layer on top of the 2026-07-29
|
|
36
36
|
* structural fix above, not a substitute for it.
|
|
37
37
|
*/
|
|
38
38
|
export { authorizeToolInvocation } from './tool-registry.js';
|
|
39
39
|
export type { ToolAuthorizationDelegate, ToolInvocationAuthorization } from './tool-registry.js';
|
|
40
40
|
export type { AnyPack, MissingTokenIds, RequiredTokenIds } from './daemon.js';
|
|
41
|
-
//# sourceMappingURL=
|
|
41
|
+
//# sourceMappingURL=composition.d.ts.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module @jini-ai/core/
|
|
2
|
+
* @module @jini-ai/core/composition
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Composition boundary for daemon assembly and authorized tool invocation.
|
|
5
|
+
* These capabilities stay off the root entry point; each has its own
|
|
6
|
+
* intended consumer:
|
|
7
7
|
*
|
|
8
8
|
* - {@link authorizeToolInvocation} so `@jini-ai/daemon`'s `ToolExecutor` can
|
|
9
9
|
* resolve a registered tool and run its authorization gate — the tool's
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
*
|
|
25
25
|
* **Correction (2026-07-19 hardening pass):** a package.json `exports` map
|
|
26
26
|
* subpath is NOT a language-level or per-consumer access modifier — Node's
|
|
27
|
-
* module resolution will happily resolve `@jini-ai/core/
|
|
27
|
+
* module resolution will happily resolve `@jini-ai/core/composition` for *any*
|
|
28
28
|
* package that depends on `@jini-ai/core`, not just the intended consumer.
|
|
29
29
|
* Earlier revisions of this doc comment claimed this boundary was
|
|
30
30
|
* "enforced" by the exports map; that was inaccurate and left the leak
|
|
31
31
|
* undetected. The actual enforcement is `scripts/check-engine-boundaries.ts`'s
|
|
32
|
-
* rule forbidding any *value* import from `@jini-ai/core/
|
|
32
|
+
* R6-internal-leak rule forbidding any *value* import from `@jini-ai/core/composition` outside
|
|
33
33
|
* `packages/daemon/**` (type-only imports of the DI-token-derivation types
|
|
34
|
-
* remain unrestricted, since they carry no runtime capability)
|
|
35
|
-
*
|
|
34
|
+
* remain unrestricted, since they carry no runtime capability). R6 checks this
|
|
35
|
+
* exact composition specifier as a second, independent layer on top of the 2026-07-29
|
|
36
36
|
* structural fix above, not a substitute for it.
|
|
37
37
|
*/
|
|
38
38
|
export { authorizeToolInvocation } from './tool-registry.js';
|
|
39
|
-
//# sourceMappingURL=
|
|
39
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Replace-by-key registry for host-owned contributions; no singleton or execution surface. */
|
|
2
|
+
export interface ContributionRegistry<TContribution> {
|
|
3
|
+
register(required: {
|
|
4
|
+
contribution: TContribution;
|
|
5
|
+
}): void;
|
|
6
|
+
/** Detached array snapshot; contribution values retain their identity. */
|
|
7
|
+
list(_required: Record<string, never>): readonly TContribution[];
|
|
8
|
+
clear(_required: Record<string, never>): void;
|
|
9
|
+
}
|
|
10
|
+
/** O(1) register/clear, O(n) list and O(n) retained entries. Replacement preserves position. */
|
|
11
|
+
export declare function createContributionRegistry<TContribution, TKey>(required: {
|
|
12
|
+
keyOf: (required: {
|
|
13
|
+
contribution: TContribution;
|
|
14
|
+
}) => TKey;
|
|
15
|
+
}): ContributionRegistry<TContribution>;
|
|
16
|
+
//# sourceMappingURL=contribution-registry.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** O(1) register/clear, O(n) list and O(n) retained entries. Replacement preserves position. */
|
|
2
|
+
export function createContributionRegistry(required) {
|
|
3
|
+
const contributions = new Map();
|
|
4
|
+
return {
|
|
5
|
+
register({ contribution }) {
|
|
6
|
+
contributions.set(required.keyOf({ contribution }), contribution);
|
|
7
|
+
},
|
|
8
|
+
list(_required) { return [...contributions.values()]; },
|
|
9
|
+
clear(_required) { contributions.clear(); },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=contribution-registry.js.map
|
package/dist/daemon.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Bindings } from './bindings.js';
|
|
2
2
|
import type { Pack } from './pack.js';
|
|
3
3
|
/**
|
|
4
|
-
* Exported (not just used locally) so `./
|
|
4
|
+
* Exported (not just used locally) so `./composition.js` can re-derive `createDaemon`'s exact
|
|
5
5
|
* compile-time "missing binding" gate for a wrapper composition function (today:
|
|
6
6
|
* `@jini-ai/server`'s `createLocalNodeDaemon`) without duplicating the type-level logic. Not
|
|
7
|
-
* re-exported from `index.ts` — see `./
|
|
7
|
+
* re-exported from `index.ts` — see `./composition.ts`'s module doc for why this stays off the
|
|
8
8
|
* public surface (same boundary pattern as `tool-registry.ts`'s `authorizeToolInvocation`).
|
|
9
9
|
*/
|
|
10
10
|
export type AnyPack = Pack<any, any, string>;
|
|
@@ -21,6 +21,9 @@ export interface Daemon<Packs extends readonly AnyPack[] = readonly AnyPack[]> {
|
|
|
21
21
|
export interface DaemonConfig<Packs extends readonly AnyPack[], BoundIds extends string> {
|
|
22
22
|
packs: Packs;
|
|
23
23
|
bindings: Bindings<BoundIds>;
|
|
24
|
+
}
|
|
25
|
+
/** Optional transport metadata, separate from required packs and bindings. */
|
|
26
|
+
export interface DaemonOptions {
|
|
24
27
|
transports?: readonly unknown[];
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
@@ -28,13 +31,15 @@ export interface DaemonConfig<Packs extends readonly AnyPack[], BoundIds extends
|
|
|
28
31
|
* declares a dep whose id isn't in `BoundIds`, the config parameter widens to
|
|
29
32
|
* require an (unsatisfiable) `__missingBindings` property carrying the exact
|
|
30
33
|
* missing-id union, so the call site fails to typecheck with that union
|
|
31
|
-
* visible in the error. Runtime: `bindings.bind()`
|
|
32
|
-
* singletons
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* visible in the error. Runtime: `bindings.bind()` rejects duplicate
|
|
35
|
+
* singletons at bind time; singleton resolution checks presence and version.
|
|
36
|
+
* Resolve every declared dependency before constructing any services, so an
|
|
37
|
+
* unused singleton still surfaces "missing binding" or a version mismatch
|
|
38
|
+
* even if the compile-time gate was bypassed with `any`. Many-token resolution
|
|
39
|
+
* preserves its existing empty-list behavior and does not check versions.
|
|
35
40
|
*/
|
|
36
41
|
export declare function createDaemon<const Packs extends readonly AnyPack[], BoundIds extends string>(config: DaemonConfig<Packs, BoundIds> & (MissingTokenIds<Packs, BoundIds> extends never ? unknown : {
|
|
37
42
|
readonly __missingBindings: MissingTokenIds<Packs, BoundIds>;
|
|
38
|
-
})): Daemon<Packs>;
|
|
43
|
+
}), _optional?: DaemonOptions): Daemon<Packs>;
|
|
39
44
|
export {};
|
|
40
45
|
//# sourceMappingURL=daemon.d.ts.map
|
package/dist/daemon.js
CHANGED
|
@@ -6,13 +6,13 @@ function makeContainer(pack, bound) {
|
|
|
6
6
|
}
|
|
7
7
|
};
|
|
8
8
|
return {
|
|
9
|
-
get(t) {
|
|
9
|
+
get({ token: t }) {
|
|
10
10
|
guard(t.id);
|
|
11
|
-
return bound.resolveOne(t);
|
|
11
|
+
return bound.resolveOne({ token: t });
|
|
12
12
|
},
|
|
13
|
-
getMany(t) {
|
|
13
|
+
getMany({ token: t }) {
|
|
14
14
|
guard(t.id);
|
|
15
|
-
return bound.resolveMany(t);
|
|
15
|
+
return bound.resolveMany({ token: t });
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
}
|
|
@@ -21,12 +21,24 @@ function makeContainer(pack, bound) {
|
|
|
21
21
|
* declares a dep whose id isn't in `BoundIds`, the config parameter widens to
|
|
22
22
|
* require an (unsatisfiable) `__missingBindings` property carrying the exact
|
|
23
23
|
* missing-id union, so the call site fails to typecheck with that union
|
|
24
|
-
* visible in the error. Runtime: `bindings.bind()`
|
|
25
|
-
* singletons
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* visible in the error. Runtime: `bindings.bind()` rejects duplicate
|
|
25
|
+
* singletons at bind time; singleton resolution checks presence and version.
|
|
26
|
+
* Resolve every declared dependency before constructing any services, so an
|
|
27
|
+
* unused singleton still surfaces "missing binding" or a version mismatch
|
|
28
|
+
* even if the compile-time gate was bypassed with `any`. Many-token resolution
|
|
29
|
+
* preserves its existing empty-list behavior and does not check versions.
|
|
28
30
|
*/
|
|
29
|
-
export function createDaemon(config) {
|
|
31
|
+
export function createDaemon(config, _optional = {}) {
|
|
32
|
+
// Validate the whole composition before service factories acquire resources;
|
|
33
|
+
// checking one pack at a time could leave earlier packs initialized on failure.
|
|
34
|
+
for (const pack of config.packs) {
|
|
35
|
+
for (const dependency of pack.deps) {
|
|
36
|
+
if (dependency.cardinality === 'one')
|
|
37
|
+
config.bindings.resolveOne({ token: dependency });
|
|
38
|
+
else
|
|
39
|
+
config.bindings.resolveMany({ token: dependency });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
const services = {};
|
|
31
43
|
for (const pack of config.packs) {
|
|
32
44
|
const container = makeContainer(pack, config.bindings);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Pure composite actor references; reject references across workspace boundaries. O(1). */
|
|
2
|
+
export declare class WorkspaceMismatchError extends Error {
|
|
3
|
+
constructor(required: {
|
|
4
|
+
message: string;
|
|
5
|
+
}, optional?: ErrorOptions);
|
|
6
|
+
}
|
|
7
|
+
export declare function appendActorReference(required: {
|
|
8
|
+
referencingRowWorkspaceId: string;
|
|
9
|
+
actorWorkspaceId: string;
|
|
10
|
+
actorId: string;
|
|
11
|
+
}, optional?: {
|
|
12
|
+
delegatedByWorkspaceId?: string | null;
|
|
13
|
+
delegatedById?: string | null;
|
|
14
|
+
}): {
|
|
15
|
+
actorWorkspaceId: string;
|
|
16
|
+
actorId: string;
|
|
17
|
+
delegatedByWorkspaceId: string | null;
|
|
18
|
+
delegatedById: string | null;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=actor-identity.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Pure composite actor references; reject references across workspace boundaries. O(1). */
|
|
2
|
+
export class WorkspaceMismatchError extends Error {
|
|
3
|
+
constructor(required, optional = {}) {
|
|
4
|
+
super(required.message, optional);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
// Composite (workspace, actor) references retain the tenant boundary even when an agent or
|
|
8
|
+
// API key acts for a delegating user. Reject mismatches before a caller persists the row.
|
|
9
|
+
// This helper performs no I/O or foreign-key checks: dependent domains own read-time orphan
|
|
10
|
+
// tolerance and reconciliation, rather than making identity population depend on storage.
|
|
11
|
+
export function appendActorReference(required, optional = {}) {
|
|
12
|
+
const { referencingRowWorkspaceId, actorWorkspaceId, actorId } = required;
|
|
13
|
+
const { delegatedByWorkspaceId, delegatedById } = optional;
|
|
14
|
+
if (actorWorkspaceId !== referencingRowWorkspaceId) {
|
|
15
|
+
throw new WorkspaceMismatchError({
|
|
16
|
+
message: `actor workspace '${actorWorkspaceId}' does not match referencing row workspace '${referencingRowWorkspaceId}'`
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (delegatedByWorkspaceId != null && delegatedByWorkspaceId !== referencingRowWorkspaceId) {
|
|
20
|
+
throw new WorkspaceMismatchError({
|
|
21
|
+
message: `delegator workspace '${delegatedByWorkspaceId}' does not match referencing row workspace '${referencingRowWorkspaceId}'`
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
actorWorkspaceId,
|
|
26
|
+
actorId,
|
|
27
|
+
delegatedByWorkspaceId: delegatedByWorkspaceId ?? null,
|
|
28
|
+
delegatedById: delegatedById ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=actor-identity.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral plan -> confirm -> execute approval ceremony.
|
|
3
|
+
* Ordering: authorize, token state, fresh actor identity, scope/hash, atomic redeem, mutation.
|
|
4
|
+
* Mutation receives the verified plan and must not independently rederive its write set.
|
|
5
|
+
* O(1) local work plus injected port costs; no lock or transactional rollback is implied.
|
|
6
|
+
*/
|
|
7
|
+
import { type Clock, type IdGenerator } from "../primitives/index.js";
|
|
8
|
+
import type { AuthorizeFn, InstanceAuthorizeFn, PrincipalKind } from "./ports.js";
|
|
9
|
+
import { type ConfirmationTokenRecord, type TokenStorePort } from "./token.js";
|
|
10
|
+
export interface GatedMutationHooks<TDetails, TResult> {
|
|
11
|
+
domain: string;
|
|
12
|
+
readPermission: string;
|
|
13
|
+
mutatePermission: string;
|
|
14
|
+
/** Workspace scope is the authorization target; for instance scope this is only an opaque
|
|
15
|
+
* audit label. A workspace grant must never stand in for authority over every workspace. */
|
|
16
|
+
scopeId: string;
|
|
17
|
+
/** Omission preserves workspace authorization; instance scope uses its own injected evaluator. */
|
|
18
|
+
scopeKind?: "workspace" | "instance";
|
|
19
|
+
computePlan(required: Record<string, never>): Promise<{
|
|
20
|
+
planHash: string;
|
|
21
|
+
details: TDetails;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Apply the verified plan, never recompute it: token redemption and any intervening mutation
|
|
25
|
+
* work separate verification from application, and this gateway takes no operation lock.
|
|
26
|
+
* Re-deriving there can apply a different write set from the one the operator approved.
|
|
27
|
+
* A mutation without plan-dependent work can ignore this argument.
|
|
28
|
+
*/
|
|
29
|
+
executeMutation(verified: {
|
|
30
|
+
planHash: string;
|
|
31
|
+
details: TDetails;
|
|
32
|
+
}): Promise<TResult>;
|
|
33
|
+
/** Resolve fresh at execution: the user itself, an agent's current delegator, or an API key's
|
|
34
|
+
* owning user. Confirm-time identity can become stale before redemption. */
|
|
35
|
+
resolveActorClassIdentity(params: {
|
|
36
|
+
principalId: string;
|
|
37
|
+
principalKind: PrincipalKind;
|
|
38
|
+
}): Promise<string | null>;
|
|
39
|
+
}
|
|
40
|
+
export interface GatewayDeps {
|
|
41
|
+
/** Required token policy: caller supplies the complete opaque token and TTL. */
|
|
42
|
+
generateToken: (required: Record<string, never>) => string;
|
|
43
|
+
ttlSeconds: number;
|
|
44
|
+
clock: Clock;
|
|
45
|
+
idGen: IdGenerator;
|
|
46
|
+
authorize: AuthorizeFn;
|
|
47
|
+
/** Missing instance authorization must deny, never fall back to a workspace-scoped grant. */
|
|
48
|
+
authorizeInstance?: InstanceAuthorizeFn;
|
|
49
|
+
tokens: TokenStorePort;
|
|
50
|
+
}
|
|
51
|
+
export interface GatewayPlan {
|
|
52
|
+
domain: string;
|
|
53
|
+
planId: string;
|
|
54
|
+
planHash: string;
|
|
55
|
+
details: unknown;
|
|
56
|
+
}
|
|
57
|
+
export declare class ForbiddenError extends Error {
|
|
58
|
+
readonly reasonCode: string;
|
|
59
|
+
constructor(required: {
|
|
60
|
+
message: string;
|
|
61
|
+
reasonCode: string;
|
|
62
|
+
}, optional?: ErrorOptions);
|
|
63
|
+
}
|
|
64
|
+
export declare class PlanStaleError extends Error {
|
|
65
|
+
constructor(required: {
|
|
66
|
+
message: string;
|
|
67
|
+
}, optional?: ErrorOptions);
|
|
68
|
+
}
|
|
69
|
+
export declare class UnauthenticatedError extends Error {
|
|
70
|
+
constructor(required: {
|
|
71
|
+
message: string;
|
|
72
|
+
}, optional?: ErrorOptions);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Share this scope decision with route authorization before operation-lock acquisition. A route
|
|
76
|
+
* that hardcodes workspace authorization can briefly acquire a lock with insufficient instance
|
|
77
|
+
* authority before the deeper gate refuses it. Missing instance wiring therefore denies outright.
|
|
78
|
+
*/
|
|
79
|
+
export declare function authorizeForHooks(required: {
|
|
80
|
+
deps: GatewayDeps;
|
|
81
|
+
hooks: GatedMutationHooks<unknown, unknown>;
|
|
82
|
+
principalId: string;
|
|
83
|
+
permission: string;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
allowed: boolean;
|
|
86
|
+
reason: string;
|
|
87
|
+
}>;
|
|
88
|
+
export declare function plan(required: {
|
|
89
|
+
deps: GatewayDeps;
|
|
90
|
+
principalId: string;
|
|
91
|
+
principalKind: PrincipalKind;
|
|
92
|
+
hooks: GatedMutationHooks<unknown, unknown>;
|
|
93
|
+
}, _optional?: Record<string, never>): Promise<GatewayPlan>;
|
|
94
|
+
export declare function confirm(required: {
|
|
95
|
+
deps: GatewayDeps;
|
|
96
|
+
principalId: string;
|
|
97
|
+
principalKind: PrincipalKind;
|
|
98
|
+
hooks: GatedMutationHooks<unknown, unknown>;
|
|
99
|
+
planId: string;
|
|
100
|
+
planHash: string;
|
|
101
|
+
}, _optional?: Record<string, never>): Promise<ConfirmationTokenRecord>;
|
|
102
|
+
/**
|
|
103
|
+
* Rejection precedence is binding when failures co-occur: fresh authorization, token state,
|
|
104
|
+
* fresh actor identity, then plan validity. Expired/redeemed tokens win over identity mismatch;
|
|
105
|
+
* identity mismatch wins over a simultaneously stale plan. Typed errors keep these outcomes
|
|
106
|
+
* distinct for callers. Only atomic redemption permits the domain mutation.
|
|
107
|
+
*/
|
|
108
|
+
export declare function execute<TResult>(required: {
|
|
109
|
+
deps: GatewayDeps;
|
|
110
|
+
principalId: string;
|
|
111
|
+
principalKind: PrincipalKind;
|
|
112
|
+
hooks: GatedMutationHooks<unknown, TResult>;
|
|
113
|
+
confirmationToken: string;
|
|
114
|
+
}, _optional?: Record<string, never>): Promise<TResult>;
|
|
115
|
+
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral plan -> confirm -> execute approval ceremony.
|
|
3
|
+
* Ordering: authorize, token state, fresh actor identity, scope/hash, atomic redeem, mutation.
|
|
4
|
+
* Mutation receives the verified plan and must not independently rederive its write set.
|
|
5
|
+
* O(1) local work plus injected port costs; no lock or transactional rollback is implied.
|
|
6
|
+
*/
|
|
7
|
+
import { nowIso } from "../primitives/index.js";
|
|
8
|
+
import { TokenAlreadyRedeemedError, TokenExpiredError, isRedeemable, mintToken, } from "./token.js";
|
|
9
|
+
export class ForbiddenError extends Error {
|
|
10
|
+
reasonCode;
|
|
11
|
+
constructor(required, optional = {}) {
|
|
12
|
+
super(required.message, optional);
|
|
13
|
+
this.reasonCode = required.reasonCode;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class PlanStaleError extends Error {
|
|
17
|
+
constructor(required, optional = {}) {
|
|
18
|
+
super(required.message, optional);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Reserved for a host that cannot establish caller identity before entering the ceremony;
|
|
22
|
+
// the gateway itself does not currently raise this error.
|
|
23
|
+
export class UnauthenticatedError extends Error {
|
|
24
|
+
constructor(required, optional = {}) {
|
|
25
|
+
super(required.message, optional);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Share this scope decision with route authorization before operation-lock acquisition. A route
|
|
30
|
+
* that hardcodes workspace authorization can briefly acquire a lock with insufficient instance
|
|
31
|
+
* authority before the deeper gate refuses it. Missing instance wiring therefore denies outright.
|
|
32
|
+
*/
|
|
33
|
+
export async function authorizeForHooks(required) {
|
|
34
|
+
const { deps, hooks, principalId, permission } = required;
|
|
35
|
+
if (hooks.scopeKind !== "instance") {
|
|
36
|
+
return deps.authorize({ principalId, permission, workspaceId: hooks.scopeId });
|
|
37
|
+
}
|
|
38
|
+
if (!deps.authorizeInstance) {
|
|
39
|
+
return { allowed: false, reason: "INSTANCE_AUTHORIZATION_NOT_CONFIGURED" };
|
|
40
|
+
}
|
|
41
|
+
return deps.authorizeInstance({ principalId, permission });
|
|
42
|
+
}
|
|
43
|
+
// Planning is read-only: check read permission and derive details, never invoke executeMutation.
|
|
44
|
+
export async function plan(required, _optional = {}) {
|
|
45
|
+
const { deps, principalId, hooks } = required;
|
|
46
|
+
const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.readPermission });
|
|
47
|
+
if (!authResult.allowed) {
|
|
48
|
+
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.readPermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
49
|
+
}
|
|
50
|
+
const { planHash, details } = await hooks.computePlan({});
|
|
51
|
+
return { domain: hooks.domain, planId: deps.idGen.newId(), planHash, details };
|
|
52
|
+
}
|
|
53
|
+
// Confirmation is a human/API-key act: an agent may redeem its delegator's approval later,
|
|
54
|
+
// but may never mint its own confirmation, regardless of its mutation permissions.
|
|
55
|
+
export async function confirm(required, _optional = {}) {
|
|
56
|
+
const { deps, principalId, principalKind, hooks, planId, planHash } = required;
|
|
57
|
+
if (principalKind === "agent") {
|
|
58
|
+
throw new ForbiddenError({ message: `agent principals may not confirm a gated mutation`, reasonCode: "AGENT_CANNOT_CONFIRM" });
|
|
59
|
+
}
|
|
60
|
+
const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.mutatePermission });
|
|
61
|
+
if (!authResult.allowed) {
|
|
62
|
+
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.mutatePermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
63
|
+
}
|
|
64
|
+
const token = mintToken({
|
|
65
|
+
planId,
|
|
66
|
+
planHash,
|
|
67
|
+
scopeId: hooks.scopeId,
|
|
68
|
+
confirmerPrincipalId: principalId,
|
|
69
|
+
now: nowIso({ clock: deps.clock }),
|
|
70
|
+
generateToken: deps.generateToken,
|
|
71
|
+
ttlSeconds: deps.ttlSeconds,
|
|
72
|
+
});
|
|
73
|
+
await deps.tokens.save({ record: token });
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Rejection precedence is binding when failures co-occur: fresh authorization, token state,
|
|
78
|
+
* fresh actor identity, then plan validity. Expired/redeemed tokens win over identity mismatch;
|
|
79
|
+
* identity mismatch wins over a simultaneously stale plan. Typed errors keep these outcomes
|
|
80
|
+
* distinct for callers. Only atomic redemption permits the domain mutation.
|
|
81
|
+
*/
|
|
82
|
+
export async function execute(required, _optional = {}) {
|
|
83
|
+
// Reevaluate authorization here; confirm-time permission is not authority to execute later.
|
|
84
|
+
const { deps, principalId, principalKind, hooks, confirmationToken } = required;
|
|
85
|
+
const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.mutatePermission });
|
|
86
|
+
if (!authResult.allowed) {
|
|
87
|
+
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.mutatePermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
88
|
+
}
|
|
89
|
+
const record = await deps.tokens.findByToken({ token: confirmationToken });
|
|
90
|
+
if (!record) {
|
|
91
|
+
throw new TokenExpiredError({ message: `confirmation token was not found` });
|
|
92
|
+
}
|
|
93
|
+
if (!isRedeemable({ record, now: nowIso({ clock: deps.clock }) })) {
|
|
94
|
+
if (record.status === "redeemed") {
|
|
95
|
+
throw new TokenAlreadyRedeemedError({ message: `confirmation token has already been redeemed` });
|
|
96
|
+
}
|
|
97
|
+
throw new TokenExpiredError({ message: `confirmation token has expired` });
|
|
98
|
+
}
|
|
99
|
+
const resolvedIdentity = await hooks.resolveActorClassIdentity({ principalId, principalKind });
|
|
100
|
+
if (resolvedIdentity !== record.confirmerPrincipalId) {
|
|
101
|
+
throw new ForbiddenError({ message: `principal '${principalId}' (kind '${principalKind}') is not the confirmer of this token`, reasonCode: "ACTOR_CLASS_MISMATCH" });
|
|
102
|
+
}
|
|
103
|
+
if (record.scopeId !== hooks.scopeId) {
|
|
104
|
+
throw new ForbiddenError({ message: `confirmation token belongs to a different scope`, reasonCode: "SCOPE_MISMATCH" });
|
|
105
|
+
}
|
|
106
|
+
// Retain these verified details through redemption so application cannot choose a new plan.
|
|
107
|
+
const { planHash, details } = await hooks.computePlan({});
|
|
108
|
+
if (planHash !== record.planHash) {
|
|
109
|
+
throw new PlanStaleError({
|
|
110
|
+
message: `the plan backing this confirmation has changed since it was confirmed; re-plan and re-confirm`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Concurrent executions can pass every earlier check; atomic redemption makes the loser fail
|
|
114
|
+
// before its domain mutation, preventing a second application of the same approval.
|
|
115
|
+
const { redeemed, record: latestRecord } = await deps.tokens.tryRedeem({ token: confirmationToken, now: nowIso({ clock: deps.clock }) });
|
|
116
|
+
if (!redeemed) {
|
|
117
|
+
if (!latestRecord || latestRecord.status !== "redeemed")
|
|
118
|
+
throw new TokenExpiredError({ message: `confirmation token has expired` });
|
|
119
|
+
throw new TokenAlreadyRedeemedError({ message: `confirmation token has already been redeemed` });
|
|
120
|
+
}
|
|
121
|
+
return hooks.executeMutation({ planHash, details });
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Authorization ports; custom actor kinds use the injected actor-identity resolver. */
|
|
2
|
+
export type PrincipalKind = "user" | "agent" | "api_key" | (string & {});
|
|
3
|
+
/** The host binds its identity evaluator as a closure; core must not import the identity feature. */
|
|
4
|
+
export type AuthorizeFn = (required: {
|
|
5
|
+
principalId: string;
|
|
6
|
+
permission: string;
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
}, optional?: {
|
|
9
|
+
entityType?: string;
|
|
10
|
+
entityId?: string;
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Instance-wide mutations need a separate evaluator, without a workspace id. Using a sentinel
|
|
17
|
+
* workspace either finds no principal or lets one tenant's admin approve a cross-tenant mutation.
|
|
18
|
+
* A workspace authorization result must never stand in for authority over the whole instance.
|
|
19
|
+
*/
|
|
20
|
+
export type InstanceAuthorizeFn = (params: {
|
|
21
|
+
principalId: string;
|
|
22
|
+
permission: string;
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
reason: string;
|
|
26
|
+
}>;
|
|
27
|
+
//# sourceMappingURL=ports.d.ts.map
|