@jini-ai/core 0.3.0 → 0.4.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/CHANGELOG.md +131 -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/composition.d.ts +41 -0
- package/dist/composition.js +39 -0
- 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 +9 -3
- package/dist/index.js +6 -1
- 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 +323 -0
- package/dist/registration-kit.js +396 -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 +60 -14
- package/dist/tool-registry.js +24 -9
- package/dist/tool-tokens.js +1 -1
- package/package.json +51 -7
- 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/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
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Opaque, single-use approval tokens. Clock, generation and TTL are host policy. */
|
|
2
|
+
export interface ConfirmationTokenRecord {
|
|
3
|
+
confirmationToken: string;
|
|
4
|
+
planHash: string;
|
|
5
|
+
scopeId: string;
|
|
6
|
+
confirmerPrincipalId: string;
|
|
7
|
+
status: 'minted' | 'redeemed' | 'expired';
|
|
8
|
+
createdAt: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TokenStorePort {
|
|
12
|
+
/** Insert only: never overwrite a previously issued token, including terminal states. */
|
|
13
|
+
save(required: {
|
|
14
|
+
record: ConfirmationTokenRecord;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
findByToken(required: {
|
|
17
|
+
token: string;
|
|
18
|
+
}): Promise<ConfirmationTokenRecord | null>;
|
|
19
|
+
/** Atomically check TTL/status and transition minted -> redeemed. */
|
|
20
|
+
tryRedeem(required: {
|
|
21
|
+
token: string;
|
|
22
|
+
now: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
redeemed: boolean;
|
|
25
|
+
record: ConfirmationTokenRecord | null;
|
|
26
|
+
}>;
|
|
27
|
+
/** Atomically transition minted -> expired without overwriting concurrent redemption. */
|
|
28
|
+
expire(required: {
|
|
29
|
+
token: string;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export declare class TokenExpiredError extends Error {
|
|
33
|
+
constructor(required: {
|
|
34
|
+
message: string;
|
|
35
|
+
}, optional?: ErrorOptions);
|
|
36
|
+
}
|
|
37
|
+
export declare class TokenAlreadyRedeemedError extends Error {
|
|
38
|
+
constructor(required: {
|
|
39
|
+
message: string;
|
|
40
|
+
}, optional?: ErrorOptions);
|
|
41
|
+
}
|
|
42
|
+
/** O(1). The complete token string comes from the injected cryptographically secure generator. */
|
|
43
|
+
export declare function mintToken(required: {
|
|
44
|
+
planId: string;
|
|
45
|
+
planHash: string;
|
|
46
|
+
scopeId: string;
|
|
47
|
+
confirmerPrincipalId: string;
|
|
48
|
+
now: string;
|
|
49
|
+
ttlSeconds: number;
|
|
50
|
+
generateToken: (required: Record<string, never>) => string;
|
|
51
|
+
}): ConfirmationTokenRecord;
|
|
52
|
+
/** O(1). The exact expiry instant is inclusive, preserving the approval lifecycle contract. */
|
|
53
|
+
export declare function isRedeemable(required: {
|
|
54
|
+
record: ConfirmationTokenRecord;
|
|
55
|
+
now: string;
|
|
56
|
+
}): boolean;
|
|
57
|
+
/** O(1) local work plus one atomic store operation. */
|
|
58
|
+
export declare function redeemToken(required: {
|
|
59
|
+
store: TokenStorePort;
|
|
60
|
+
token: string;
|
|
61
|
+
now: string;
|
|
62
|
+
}): Promise<ConfirmationTokenRecord>;
|
|
63
|
+
/** Explicit revocation; now is retained for compatibility with lifecycle caller records. */
|
|
64
|
+
export declare function expireToken(required: {
|
|
65
|
+
store: TokenStorePort;
|
|
66
|
+
token: string;
|
|
67
|
+
now: string;
|
|
68
|
+
}): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Instance-owned test adapter. Operations are O(1); space O(n) issued tokens.
|
|
71
|
+
* No await splits conditional transitions. Records are returned as detached values.
|
|
72
|
+
* Hosts must supply retention/cleanup for a long-lived store.
|
|
73
|
+
*/
|
|
74
|
+
export declare class InMemoryTokenStore implements TokenStorePort {
|
|
75
|
+
constructor(_required: Record<string, never>);
|
|
76
|
+
private readonly recordsByToken;
|
|
77
|
+
save(required: {
|
|
78
|
+
record: ConfirmationTokenRecord;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
findByToken(required: {
|
|
81
|
+
token: string;
|
|
82
|
+
}): Promise<ConfirmationTokenRecord | null>;
|
|
83
|
+
tryRedeem(required: {
|
|
84
|
+
token: string;
|
|
85
|
+
now: string;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
redeemed: boolean;
|
|
88
|
+
record: ConfirmationTokenRecord | null;
|
|
89
|
+
}>;
|
|
90
|
+
expire(required: {
|
|
91
|
+
token: string;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
count(_required: Record<string, never>): Promise<number>;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class TokenExpiredError extends Error {
|
|
2
|
+
constructor(required, optional = {}) {
|
|
3
|
+
super(required.message, optional);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export class TokenAlreadyRedeemedError extends Error {
|
|
7
|
+
constructor(required, optional = {}) {
|
|
8
|
+
super(required.message, optional);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** O(1). The complete token string comes from the injected cryptographically secure generator. */
|
|
12
|
+
export function mintToken(required) {
|
|
13
|
+
const timestamp = Date.parse(required.now);
|
|
14
|
+
if (!Number.isFinite(timestamp) || !Number.isFinite(required.ttlSeconds) || required.ttlSeconds <= 0) {
|
|
15
|
+
throw new RangeError('token timestamp must be valid and TTL must be positive and finite');
|
|
16
|
+
}
|
|
17
|
+
const expiresAt = new Date(timestamp + required.ttlSeconds * 1000).toISOString();
|
|
18
|
+
// Exact host-selected TTL, without jitter: approval expiration is a user-visible boundary.
|
|
19
|
+
const confirmationToken = required.generateToken({});
|
|
20
|
+
if (typeof confirmationToken !== 'string' || confirmationToken.length === 0) {
|
|
21
|
+
throw new Error('token generator must return a non-empty string');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
confirmationToken, planHash: required.planHash, scopeId: required.scopeId,
|
|
25
|
+
confirmerPrincipalId: required.confirmerPrincipalId, status: 'minted',
|
|
26
|
+
createdAt: required.now, expiresAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** O(1). The exact expiry instant is inclusive, preserving the approval lifecycle contract. */
|
|
30
|
+
export function isRedeemable(required) {
|
|
31
|
+
return required.record.status === 'minted' && Date.parse(required.now) <= Date.parse(required.record.expiresAt);
|
|
32
|
+
}
|
|
33
|
+
/** O(1) local work plus one atomic store operation. */
|
|
34
|
+
export async function redeemToken(required) {
|
|
35
|
+
// Gateways may use the port directly to interleave actor/authorization/hash checks in their own
|
|
36
|
+
// required order; this wrapper owns the standalone lifecycle error contract. Missing and expired
|
|
37
|
+
// approvals share an error type, while a spent approval reports its distinct terminal state.
|
|
38
|
+
const { redeemed, record } = await required.store.tryRedeem({ token: required.token, now: required.now });
|
|
39
|
+
if (redeemed && record)
|
|
40
|
+
return record;
|
|
41
|
+
if (!record)
|
|
42
|
+
throw new TokenExpiredError({ message: 'confirmation token was not found' });
|
|
43
|
+
if (record.status === 'redeemed')
|
|
44
|
+
throw new TokenAlreadyRedeemedError({ message: 'confirmation token has already been redeemed' });
|
|
45
|
+
throw new TokenExpiredError({ message: 'confirmation token has expired' });
|
|
46
|
+
}
|
|
47
|
+
/** Explicit revocation; now is retained for compatibility with lifecycle caller records. */
|
|
48
|
+
export async function expireToken(required) {
|
|
49
|
+
// Revocation can only advance minted -> expired; it must never un-redeem a spent approval.
|
|
50
|
+
await required.store.expire({ token: required.token });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Instance-owned test adapter. Operations are O(1); space O(n) issued tokens.
|
|
54
|
+
* No await splits conditional transitions. Records are returned as detached values.
|
|
55
|
+
* Hosts must supply retention/cleanup for a long-lived store.
|
|
56
|
+
*/
|
|
57
|
+
export class InMemoryTokenStore {
|
|
58
|
+
constructor(_required) { }
|
|
59
|
+
recordsByToken = new Map();
|
|
60
|
+
async save(required) {
|
|
61
|
+
const record = required.record;
|
|
62
|
+
if (this.recordsByToken.has(record.confirmationToken))
|
|
63
|
+
throw new Error('confirmation token already exists');
|
|
64
|
+
this.recordsByToken.set(record.confirmationToken, { ...record });
|
|
65
|
+
}
|
|
66
|
+
async findByToken(required) {
|
|
67
|
+
const record = this.recordsByToken.get(required.token);
|
|
68
|
+
return record ? { ...record } : null;
|
|
69
|
+
}
|
|
70
|
+
async tryRedeem(required) {
|
|
71
|
+
const record = this.recordsByToken.get(required.token);
|
|
72
|
+
if (!record)
|
|
73
|
+
return { redeemed: false, record: null };
|
|
74
|
+
if (!isRedeemable({ record, now: required.now }))
|
|
75
|
+
return { redeemed: false, record: { ...record } };
|
|
76
|
+
const updated = { ...record, status: 'redeemed' };
|
|
77
|
+
this.recordsByToken.set(required.token, updated);
|
|
78
|
+
return { redeemed: true, record: { ...updated } };
|
|
79
|
+
}
|
|
80
|
+
async expire(required) {
|
|
81
|
+
const record = this.recordsByToken.get(required.token);
|
|
82
|
+
if (record?.status === 'minted')
|
|
83
|
+
this.recordsByToken.set(required.token, { ...record, status: 'expired' });
|
|
84
|
+
}
|
|
85
|
+
async count(_required) { return this.recordsByToken.size; }
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=token.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './token.js';
|
|
2
2
|
export * from './pack.js';
|
|
3
3
|
export * from './bindings.js';
|
|
4
|
-
export type { Daemon, DaemonConfig } from './daemon.js';
|
|
4
|
+
export type { Daemon, DaemonConfig, DaemonOptions } from './daemon.js';
|
|
5
5
|
export { createDaemon } from './daemon.js';
|
|
6
6
|
export type { PackDisposalFailure } from './pack-lifecycle.js';
|
|
7
7
|
export { disposePacks, registerPackTools } from './pack-lifecycle.js';
|
|
@@ -9,7 +9,13 @@ export * from './redact.js';
|
|
|
9
9
|
export * from './api-token-auth.js';
|
|
10
10
|
export * from './origin-validation.js';
|
|
11
11
|
export * from './principal.js';
|
|
12
|
-
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
13
|
-
export { createToolRegistry, ToolInputError } from './tool-registry.js';
|
|
12
|
+
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
13
|
+
export { createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
|
|
14
14
|
export * from './tool-tokens.js';
|
|
15
|
+
export * from './gated-mutations.js';
|
|
16
|
+
export * from './model-facing-tool-errors.js';
|
|
17
|
+
export * from './contribution-registry.js';
|
|
18
|
+
export * from './naming.js';
|
|
19
|
+
export type { AgentToolSideEffect, AgentToolActorClassRule, AgentToolDefinition } from './agent-tools.js';
|
|
20
|
+
export * from "./registration-kit.js";
|
|
15
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,11 @@ export * from './redact.js';
|
|
|
7
7
|
export * from './api-token-auth.js';
|
|
8
8
|
export * from './origin-validation.js';
|
|
9
9
|
export * from './principal.js';
|
|
10
|
-
export { createToolRegistry, ToolInputError } from './tool-registry.js';
|
|
10
|
+
export { createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
|
|
11
11
|
export * from './tool-tokens.js';
|
|
12
|
+
export * from './gated-mutations.js';
|
|
13
|
+
export * from './model-facing-tool-errors.js';
|
|
14
|
+
export * from './contribution-registry.js';
|
|
15
|
+
export * from './naming.js';
|
|
16
|
+
export * from "./registration-kit.js";
|
|
12
17
|
//# sourceMappingURL=index.js.map
|