@jini-ai/core 0.3.1 → 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/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/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 +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 +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 +32 -16
- package/dist/tool-registry.js +11 -11
- package/dist/tool-tokens.js +1 -1
- package/package.json +55 -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
|
@@ -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';
|
|
12
|
+
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
13
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
|
@@ -9,4 +9,9 @@ export * from './origin-validation.js';
|
|
|
9
9
|
export * from './principal.js';
|
|
10
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered domain-owned allowlists for safe model-facing failures. Unknown throws are unchanged.
|
|
3
|
+
* Rule evaluation is O(r); container wrapping O(n), with no I/O or reporting dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { type ToolHandler, type ToolRegistration } from "./tool-registry.js";
|
|
6
|
+
export interface ModelFacingErrorRule {
|
|
7
|
+
readonly error: abstract new (...args: never[]) => Error;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly message?: string;
|
|
10
|
+
readonly guidance?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function forbiddenRule(required: {
|
|
13
|
+
domainPrefix: string;
|
|
14
|
+
error: ModelFacingErrorRule["error"];
|
|
15
|
+
}): ModelFacingErrorRule;
|
|
16
|
+
export declare function reclassifyToolError(required: {
|
|
17
|
+
err: unknown;
|
|
18
|
+
rules: readonly ModelFacingErrorRule[];
|
|
19
|
+
}): unknown;
|
|
20
|
+
export declare function withModelFacingErrors(required: {
|
|
21
|
+
handlers: Readonly<Record<string, ToolHandler>>;
|
|
22
|
+
rules: readonly ModelFacingErrorRule[];
|
|
23
|
+
}): Record<string, ToolHandler>;
|
|
24
|
+
export declare function withModelFacingRegistrationErrors(required: {
|
|
25
|
+
registrations: readonly ToolRegistration[];
|
|
26
|
+
rules: readonly ModelFacingErrorRule[];
|
|
27
|
+
}): ToolRegistration[];
|
|
28
|
+
export interface CallerSafeErrorRule {
|
|
29
|
+
readonly error: abstract new (...args: never[]) => Error;
|
|
30
|
+
readonly message?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function callerSafeErrorMessage(required: {
|
|
33
|
+
err: unknown;
|
|
34
|
+
rules: readonly CallerSafeErrorRule[];
|
|
35
|
+
fallback: string;
|
|
36
|
+
}): string;
|
|
37
|
+
//# sourceMappingURL=model-facing-tool-errors.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered domain-owned allowlists for safe model-facing failures. Unknown throws are unchanged.
|
|
3
|
+
* Rule evaluation is O(r); container wrapping O(n), with no I/O or reporting dependencies.
|
|
4
|
+
*/
|
|
5
|
+
// Tool transports redact unknown throws as internal failures. Leaving known domain refusals there
|
|
6
|
+
// made validation/not-found/permission errors look like identical crashes, preventing recovery.
|
|
7
|
+
// Domains explicitly own which classes/messages are safe to expose, including anonymous callers:
|
|
8
|
+
// stacks, internal paths, SQL, credentials, and another tenant's data must remain unlisted/redacted.
|
|
9
|
+
import { ToolInputError } from "./tool-registry.js";
|
|
10
|
+
export function forbiddenRule(required) {
|
|
11
|
+
return { error: required.error, code: `${required.domainPrefix}_FORBIDDEN` };
|
|
12
|
+
}
|
|
13
|
+
// First match wins: list subclasses before superclasses. Unmatched values must retain identity so
|
|
14
|
+
// the transport's redaction remains the default; already-classified ToolInputError also stays intact
|
|
15
|
+
// to avoid doubling a prefix attached by schema/recovery wrapping.
|
|
16
|
+
export function reclassifyToolError(required) {
|
|
17
|
+
const { err, rules } = required;
|
|
18
|
+
if (err instanceof ToolInputError)
|
|
19
|
+
return err;
|
|
20
|
+
for (const rule of rules) {
|
|
21
|
+
if (err instanceof rule.error) {
|
|
22
|
+
const message = `${rule.code}: ${rule.message ?? err.message}`;
|
|
23
|
+
return new ToolInputError({ message: rule.guidance ? `${message}. ${rule.guidance}` : message });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
// Wrap the whole map once: a correct per-handler converter previously reached only one handler
|
|
29
|
+
// while sibling operations still leaked into the internal-error redactor. Central wrapping has no
|
|
30
|
+
// omitted call-site arm, and successful results pass through untouched.
|
|
31
|
+
export function withModelFacingErrors(required) {
|
|
32
|
+
const { handlers, rules } = required;
|
|
33
|
+
return Object.fromEntries(Object.entries(handlers).map(([toolId, handler]) => [
|
|
34
|
+
toolId,
|
|
35
|
+
async (ctx, optional = {}) => {
|
|
36
|
+
try {
|
|
37
|
+
return await handler(ctx, optional);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw reclassifyToolError({ err, rules });
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
// Registration-array builders need the same blanket coverage as handler maps. Preserve descriptor,
|
|
46
|
+
// policy, ordering, and successful results; only the rejection path is reclassified.
|
|
47
|
+
export function withModelFacingRegistrationErrors(required) {
|
|
48
|
+
const { registrations, rules } = required;
|
|
49
|
+
return registrations.map((registration) => ({
|
|
50
|
+
...registration,
|
|
51
|
+
handler: (async (ctx, optional = {}) => {
|
|
52
|
+
try {
|
|
53
|
+
return await registration.handler(ctx, optional);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw reclassifyToolError({ err, rules });
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
// Unknown classes, raw driver errors, and non-Error throws get fixed fallback text, never raw error
|
|
62
|
+
// content. The fallback itself must not be derived from err; first matching rule still wins.
|
|
63
|
+
export function callerSafeErrorMessage(required) {
|
|
64
|
+
const { err, rules, fallback } = required;
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
if (err instanceof rule.error)
|
|
67
|
+
return rule.message ?? err.message;
|
|
68
|
+
}
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=model-facing-tool-errors.js.map
|
package/dist/naming.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bounded asynchronous collision search; naming and copy wording are required host policy. */
|
|
2
|
+
export declare const MAX_SUFFIX_ATTEMPTS = 1000;
|
|
3
|
+
export interface NamingPolicy {
|
|
4
|
+
isTaken: (required: {
|
|
5
|
+
candidate: string;
|
|
6
|
+
}) => Promise<boolean>;
|
|
7
|
+
withSuffix: (required: {
|
|
8
|
+
base: string;
|
|
9
|
+
suffix: number;
|
|
10
|
+
}) => string;
|
|
11
|
+
exhaustionMessage: (required: {
|
|
12
|
+
base: string;
|
|
13
|
+
maxAttempts: number;
|
|
14
|
+
}) => string;
|
|
15
|
+
}
|
|
16
|
+
export interface AvailableNameOptions {
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
onExhausted?: (required: {
|
|
19
|
+
base: string;
|
|
20
|
+
maxAttempts: number;
|
|
21
|
+
}) => never;
|
|
22
|
+
}
|
|
23
|
+
/** O(k) sequential predicate calls, O(1) local space, k <= maxAttempts. */
|
|
24
|
+
export declare function deriveAvailableName(required: NamingPolicy & {
|
|
25
|
+
base: string;
|
|
26
|
+
}, optional?: AvailableNameOptions): Promise<string>;
|
|
27
|
+
/** O(k) plus at most one base lookup. A trailing integer is a counter only if its base exists. */
|
|
28
|
+
export declare function deriveDuplicateName(required: NamingPolicy & {
|
|
29
|
+
sourceName: string;
|
|
30
|
+
}, optional?: AvailableNameOptions): Promise<string>;
|
|
31
|
+
//# sourceMappingURL=naming.d.ts.map
|
package/dist/naming.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Bounded asynchronous collision search; naming and copy wording are required host policy. */
|
|
2
|
+
export const MAX_SUFFIX_ATTEMPTS = 1000;
|
|
3
|
+
/** O(k) sequential predicate calls, O(1) local space, k <= maxAttempts. */
|
|
4
|
+
export async function deriveAvailableName(required, optional = {}) {
|
|
5
|
+
// isTaken may also reject reserved/invalid candidates. withSuffix owns formatting and length caps;
|
|
6
|
+
// shorten the base to make room rather than truncating the counter and repeating a taken candidate.
|
|
7
|
+
const maxAttempts = optional.maxAttempts ?? MAX_SUFFIX_ATTEMPTS;
|
|
8
|
+
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
|
|
9
|
+
throw new RangeError('maxAttempts must be a positive safe integer');
|
|
10
|
+
for (let suffix = 1; suffix <= maxAttempts; suffix += 1) {
|
|
11
|
+
const candidate = suffix === 1 ? required.base : required.withSuffix({ base: required.base, suffix });
|
|
12
|
+
if (!(await required.isTaken({ candidate })))
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
if (optional.onExhausted)
|
|
16
|
+
optional.onExhausted({ base: required.base, maxAttempts });
|
|
17
|
+
throw new Error(required.exhaustionMessage({ base: required.base, maxAttempts }));
|
|
18
|
+
}
|
|
19
|
+
const TRAILING_NUMBER_PATTERN = /^(.*\S) \d+$/;
|
|
20
|
+
// Require a non-space base: a bare number has nothing meaningful to strip. Existing-base evidence
|
|
21
|
+
// distinguishes a copy counter ("Landing 2") from a real title ("Blog 2024" with no "Blog" row).
|
|
22
|
+
/** O(k) plus at most one base lookup. A trailing integer is a counter only if its base exists. */
|
|
23
|
+
export async function deriveDuplicateName(required, optional = {}) {
|
|
24
|
+
const strippedBase = TRAILING_NUMBER_PATTERN.exec(required.sourceName)?.[1];
|
|
25
|
+
const base = strippedBase !== undefined && await required.isTaken({ candidate: strippedBase })
|
|
26
|
+
? strippedBase : required.sourceName;
|
|
27
|
+
return deriveAvailableName({
|
|
28
|
+
// A copy must never reuse the source's name, even if the external predicate overlooks its row.
|
|
29
|
+
...required, base,
|
|
30
|
+
isTaken: ({ candidate }) => candidate === required.sourceName ? Promise.resolve(true) : required.isTaken({ candidate }),
|
|
31
|
+
}, optional);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=naming.js.map
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Generalized from an upstream flat daemon module: the origin hardcoded its
|
|
11
11
|
* host product's env-var names, now fields on
|
|
12
|
-
* {@link OriginValidationEnvConfig} — see `
|
|
12
|
+
* {@link OriginValidationEnvConfig} — see `archived provenance ledger` for the exact
|
|
13
13
|
* mapping. One function, a bypass for a specific product's own
|
|
14
14
|
* browser-extension-driven ingest route, was deliberately **not** ported —
|
|
15
15
|
* it names a specific product route and product feature with no generic
|
|
16
|
-
* equivalent; see `
|
|
16
|
+
* equivalent; see `archived provenance ledger`.
|
|
17
17
|
*/
|
|
18
|
+
import type { Logger } from "./primitives/index.js";
|
|
18
19
|
export interface ParsedHostHeader {
|
|
19
20
|
hostname: string;
|
|
20
21
|
host: string;
|
|
@@ -36,44 +37,88 @@ export interface OriginValidationEnvConfig {
|
|
|
36
37
|
/** Env var carrying the host the daemon itself is bound to (default `'127.0.0.1'`). */
|
|
37
38
|
bindHostEnvVar: string;
|
|
38
39
|
}
|
|
40
|
+
/** Host-supplied environment snapshot, shared by origin validation helpers. */
|
|
41
|
+
export interface OriginValidationArgs {
|
|
42
|
+
config: OriginValidationEnvConfig;
|
|
43
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
44
|
+
}
|
|
45
|
+
/** Optional diagnostic sink; validation never reads an ambient logger. */
|
|
46
|
+
export type OriginValidationLoggerPort = Pick<Logger, "warn">;
|
|
39
47
|
/**
|
|
40
48
|
* Parses {@link OriginValidationEnvConfig.allowedOriginsEnvVar} into normalized origins.
|
|
41
49
|
*
|
|
42
50
|
* Deliberately never throws — this is on the hot path of {@link isLocalSameOrigin}, called fresh
|
|
43
51
|
* on every same-origin decision a host makes. A malformed entry is dropped and logged instead of
|
|
44
|
-
* aborting the whole parse
|
|
45
|
-
* incident history this mirrors: a request-time throw here used to turn one config typo into
|
|
52
|
+
* aborting the whole parse. A request-time throw used to turn one config typo into
|
|
46
53
|
* either a 500 on every request, or — on a route with no enclosing catch — an unhandled
|
|
47
|
-
* rejection
|
|
54
|
+
* rejection: hand-mounted attachment routes and provider catch-all routes had no common
|
|
55
|
+
* enclosing catch. Dropping the malformed entry degrades to one fewer trusted origin,
|
|
56
|
+
* never every request breaking. Call {@link assertValidAllowedOrigins} once at host startup instead, to fail loudly
|
|
48
57
|
* before any request-serving traffic exists.
|
|
49
58
|
*/
|
|
50
|
-
export declare function configuredAllowedOrigins(config:
|
|
59
|
+
export declare function configuredAllowedOrigins({ config, env }: OriginValidationArgs, { logger }?: {
|
|
60
|
+
logger?: OriginValidationLoggerPort | undefined;
|
|
61
|
+
}): string[];
|
|
51
62
|
/**
|
|
52
63
|
* Boot-time companion to {@link configuredAllowedOrigins}: throws, naming every malformed entry,
|
|
53
64
|
* instead of silently dropping them. Call once, early in host startup, before the HTTP server
|
|
54
|
-
* accepts connections.
|
|
65
|
+
* accepts connections. A misconfiguration must fail boot loudly rather than quietly serving with
|
|
66
|
+
* a smaller-than-intended allow-list, or throwing on whichever request first reaches the guard.
|
|
55
67
|
*/
|
|
56
|
-
export declare function assertValidAllowedOrigins(config
|
|
68
|
+
export declare function assertValidAllowedOrigins({ config, env }: OriginValidationArgs): void;
|
|
57
69
|
/** The `host` (hostname:port) component of each configured allowed origin. */
|
|
58
|
-
export declare function configuredAllowedHosts(origins
|
|
70
|
+
export declare function configuredAllowedHosts({ origins }: {
|
|
71
|
+
origins: string[];
|
|
72
|
+
}): string[];
|
|
59
73
|
/** The set of browser-facing ports considered "local": the daemon's own port plus an optional configured web port. */
|
|
60
|
-
export declare function allowedBrowserPorts(config
|
|
74
|
+
export declare function allowedBrowserPorts({ config, port, env }: OriginValidationArgs & {
|
|
75
|
+
port: number | string | null | undefined;
|
|
76
|
+
}): number[];
|
|
61
77
|
/** Parses a raw `Host` header value into hostname/host/port, or `null` when unparseable. */
|
|
62
|
-
export declare function parseHostHeader(value
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
export declare function parseHostHeader({ value }: {
|
|
79
|
+
value: unknown;
|
|
80
|
+
}): ParsedHostHeader | null;
|
|
81
|
+
/** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
|
|
82
|
+
* Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
|
|
83
|
+
* for SSRF address classification; private addresses are allowed here for local development. */
|
|
84
|
+
export declare function isPrivateIpv4({ hostname }: {
|
|
85
|
+
hostname: unknown;
|
|
86
|
+
}): boolean;
|
|
65
87
|
/** Whether `hostname` is a literal IPv4 address or bracketed IPv6 literal (as opposed to a DNS name). */
|
|
66
|
-
export declare function isIpLiteralHostname(hostname
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
export declare function isIpLiteralHostname({ hostname }: {
|
|
89
|
+
hostname: unknown;
|
|
90
|
+
}): boolean;
|
|
91
|
+
/** Whether `hostname` is loopback, unspecified, or a private-LAN address.
|
|
92
|
+
* Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
|
|
93
|
+
export declare function isLoopbackOrPrivateLanHost({ hostname }: {
|
|
94
|
+
hostname: unknown;
|
|
95
|
+
}): boolean;
|
|
69
96
|
/** Whether a request's `Host` header names a locally-served or explicitly allow-listed host. */
|
|
70
|
-
export declare function isAllowedBrowserHost(
|
|
97
|
+
export declare function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowedOrigins }: {
|
|
98
|
+
config: OriginValidationEnvConfig;
|
|
99
|
+
hostHeader: unknown;
|
|
100
|
+
ports: number[];
|
|
101
|
+
bindHost: string;
|
|
102
|
+
extraAllowedOrigins: string[];
|
|
103
|
+
}): boolean;
|
|
71
104
|
/** Whether an `Origin` header value names a locally-served or explicitly allow-listed origin. */
|
|
72
|
-
export declare function isAllowedBrowserOrigin(
|
|
105
|
+
export declare function isAllowedBrowserOrigin({ origin, hostHeader, ports, bindHost, extraAllowedOrigins }: {
|
|
106
|
+
config: OriginValidationEnvConfig;
|
|
107
|
+
origin: unknown;
|
|
108
|
+
hostHeader: unknown;
|
|
109
|
+
ports: number[];
|
|
110
|
+
bindHost: string;
|
|
111
|
+
extraAllowedOrigins: string[];
|
|
112
|
+
}): boolean;
|
|
73
113
|
/**
|
|
74
114
|
* The top-level same-origin gate: is `req` a request the daemon should treat
|
|
75
115
|
* as coming from one of its own locally-served (or explicitly allow-listed)
|
|
76
116
|
* pages.
|
|
77
117
|
*/
|
|
78
|
-
export declare function isLocalSameOrigin(config
|
|
118
|
+
export declare function isLocalSameOrigin({ config, req, port, env }: OriginValidationArgs & {
|
|
119
|
+
req: RequestWithOriginHeaders;
|
|
120
|
+
port: number | string | null | undefined;
|
|
121
|
+
}, { logger }?: {
|
|
122
|
+
logger?: OriginValidationLoggerPort | undefined;
|
|
123
|
+
}): boolean;
|
|
79
124
|
//# sourceMappingURL=origin-validation.d.ts.map
|