@awiki/dsh-pap 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +197 -2
  3. package/cordis.patch.yml +15 -0
  4. package/docs/verification.md +217 -0
  5. package/lib/auth-challenge.d.ts +19 -0
  6. package/lib/auth-challenge.js +136 -0
  7. package/lib/browser-client.js +65 -0
  8. package/lib/browser-client.js.map +6 -0
  9. package/lib/callback-handoff.d.ts +55 -0
  10. package/lib/callback-handoff.js +33 -0
  11. package/lib/client/controller.d.ts +107 -0
  12. package/lib/client/controller.js +106 -0
  13. package/lib/client/index.d.ts +6 -0
  14. package/lib/client/index.js +28 -0
  15. package/lib/client/views.d.ts +5 -0
  16. package/lib/client/views.js +28 -0
  17. package/lib/client.d.ts +108 -0
  18. package/lib/client.js +833 -0
  19. package/lib/cooldown.d.ts +11 -0
  20. package/lib/cooldown.js +88 -0
  21. package/lib/credential-history.d.ts +33 -0
  22. package/lib/credential-history.js +109 -0
  23. package/lib/delegation.d.ts +16 -0
  24. package/lib/delegation.js +59 -0
  25. package/lib/discovery.d.ts +8 -0
  26. package/lib/discovery.js +227 -0
  27. package/lib/errors.d.ts +14 -0
  28. package/lib/errors.js +17 -0
  29. package/lib/file-secrets.d.ts +14 -0
  30. package/lib/file-secrets.js +148 -0
  31. package/lib/host-secrets.d.ts +16 -0
  32. package/lib/host-secrets.js +22 -0
  33. package/lib/host.d.ts +83 -0
  34. package/lib/host.js +1 -0
  35. package/lib/index.d.ts +21 -0
  36. package/lib/index.js +13 -0
  37. package/lib/keyring.d.ts +10 -0
  38. package/lib/keyring.js +64 -0
  39. package/lib/management-remote.d.ts +13 -0
  40. package/lib/management-remote.js +69 -0
  41. package/lib/network.d.ts +23 -0
  42. package/lib/network.js +91 -0
  43. package/lib/plugin.d.ts +75 -0
  44. package/lib/plugin.js +473 -0
  45. package/lib/product-api.d.ts +89 -0
  46. package/lib/product-api.js +25 -0
  47. package/lib/product-operations.d.ts +16 -0
  48. package/lib/product-operations.js +120 -0
  49. package/lib/protocol-adapter.d.ts +90 -0
  50. package/lib/protocol-adapter.js +75 -0
  51. package/lib/remote.d.ts +3 -0
  52. package/lib/remote.js +17 -0
  53. package/lib/store.d.ts +14 -0
  54. package/lib/store.js +128 -0
  55. package/lib/strict-json.d.ts +2 -0
  56. package/lib/strict-json.js +111 -0
  57. package/lib/tools.d.ts +5 -0
  58. package/lib/tools.js +29 -0
  59. package/lib/types.d.ts +202 -0
  60. package/lib/types.js +1 -0
  61. package/lib/util.d.ts +17 -0
  62. package/lib/util.js +66 -0
  63. package/package.json +155 -4
@@ -0,0 +1,16 @@
1
+ import type { Company, HttpResponse, Operation, PapState, ProductOperationApproval, Route } from './types.js';
2
+ import type { PapOperationRequest } from './product-api.js';
3
+ export declare function operationRef(company: Company, route: Route): string;
4
+ export declare function findOperation(state: PapState, reference: string): {
5
+ company: Company;
6
+ route: Route;
7
+ };
8
+ export declare function publicOperations(state: PapState, enabledOnly?: boolean): object[];
9
+ export declare function assertApproval(state: PapState, reference: string, expected: ProductOperationApproval): void;
10
+ export declare function prepareOperation(state: PapState, input: PapOperationRequest): {
11
+ operation: Operation;
12
+ approval: ProductOperationApproval;
13
+ };
14
+ export declare function privateValues(state: PapState, includeOAuthState?: boolean): string[];
15
+ export declare function sanitizePublicValue(value: unknown, secrets: readonly string[]): unknown;
16
+ export declare function publicResponse(response: HttpResponse, secrets?: readonly string[]): object;
@@ -0,0 +1,120 @@
1
+ import { PapError, requirePap } from './errors.js';
2
+ import { httpsUrl, htu, sha256 } from './util.js';
3
+ import { strictJson } from './strict-json.js';
4
+ export function operationRef(company, route) {
5
+ return `op_${sha256(JSON.stringify([company.ref, route.method, route.template, route.resource, route.parameters ?? [], route.channel]))}`;
6
+ }
7
+ function supported(company, route) {
8
+ const reserved = [...company.sources.map(x => x.url), company.metadata?.token_endpoint, company.metadata?.authorization_endpoint, company.metadata?.revocation_endpoint].filter((value) => Boolean(value));
9
+ return route.channel !== 'mcp' && !reserved.some(value => htu(value) === htu(route.template.replace(/\{[^{}]+\}/g, '__parameter__')));
10
+ }
11
+ export function findOperation(state, reference) {
12
+ for (const company of Object.values(state.companies))
13
+ for (const route of company.routes) {
14
+ if (operationRef(company, route) === reference && supported(company, route))
15
+ return { company, route };
16
+ }
17
+ throw new PapError('unsafe_destination');
18
+ }
19
+ export function publicOperations(state, enabledOnly = false) {
20
+ const entries = [];
21
+ for (const company of Object.values(state.companies))
22
+ for (const route of company.routes) {
23
+ if (!supported(company, route))
24
+ continue;
25
+ const reference = operationRef(company, route);
26
+ const approval = state.product?.operations[reference];
27
+ if (enabledOnly && !approval)
28
+ continue;
29
+ entries.push({ operationRef: reference, companyRef: company.ref, method: route.method, path: new URL(route.template).pathname.replace(/%7b/gi, '{').replace(/%7d/gi, '}'),
30
+ pathParameters: [...route.template.matchAll(/\{([A-Za-z0-9_.-]+)\}/g)].map(x => x[1]),
31
+ queryParameters: (route.parameters ?? []).filter(x => x.in === 'query').map(x => ({ name: x.name, required: x.required })),
32
+ enabled: Boolean(approval), account: approval ? Boolean(state.sessions[approval.sessionRef]?.accountSlot) : false });
33
+ }
34
+ return entries;
35
+ }
36
+ export function assertApproval(state, reference, expected) {
37
+ const current = state.product?.operations[reference];
38
+ requirePap(current && JSON.stringify(current) === JSON.stringify(expected), 'permission_denied');
39
+ const { company, route } = findOperation(state, reference);
40
+ requirePap(company.ref === current.companyRef && route.method === current.method && route.template === current.template && route.resource === current.resource, 'permission_denied');
41
+ const session = state.sessions[current.sessionRef];
42
+ const subject = session && state.subjects[session.subjectRef];
43
+ requirePap(session?.connected && subject?.companyRef === company.ref, 'disconnected');
44
+ }
45
+ export function prepareOperation(state, input) {
46
+ const approval = state.product?.operations[input.operationRef];
47
+ requirePap(approval, 'permission_denied');
48
+ assertApproval(state, input.operationRef, approval);
49
+ const { route } = findOperation(state, input.operationRef);
50
+ const pathNames = [...route.template.matchAll(/\{([A-Za-z0-9_.-]+)\}/g)].map(x => x[1]);
51
+ const path = input.path ?? {};
52
+ const query = input.query ?? {};
53
+ requirePap(Object.keys(path).every(key => pathNames.includes(key)) && pathNames.every(key => typeof path[key] === 'string'), 'unsafe_destination');
54
+ let destination = route.template.replace(/\{([A-Za-z0-9_.-]+)\}/g, (_all, key) => {
55
+ const value = path[key];
56
+ requirePap(value.length > 0 && value !== '.' && value !== '..' && !/[\/%\\\x00-\x20\x7f]/.test(value), 'unsafe_destination');
57
+ return encodeURIComponent(value);
58
+ });
59
+ requirePap(!/[{}]/.test(destination), 'unsupported_profile');
60
+ const url = httpsUrl(destination);
61
+ requirePap(!url.search, 'unsafe_destination');
62
+ const allowedQuery = (route.parameters ?? []).filter(x => x.in === 'query');
63
+ requirePap(Object.keys(query).every(key => allowedQuery.some(x => x.name === key)) && allowedQuery.filter(x => x.required).every(x => query[x.name] !== undefined), 'unsafe_destination');
64
+ for (const [key, value] of Object.entries(query)) {
65
+ requirePap(!/[\x00-\x1f\x7f]/.test(value), 'unsafe_destination');
66
+ url.searchParams.set(key, value);
67
+ }
68
+ requirePap(url.href.length <= 8192, 'unsafe_destination');
69
+ const body = input.body === undefined ? undefined : JSON.stringify(input.body);
70
+ requirePap(body === undefined || ['POST', 'PUT', 'PATCH', 'DELETE'].includes(route.method) && Buffer.byteLength(body) <= 65536, 'permission_denied');
71
+ return { approval: structuredClone(approval), operation: { url: url.href, method: route.method, resource: route.resource, scopes: [...approval.scopes],
72
+ ...(body === undefined ? {} : { body, headers: { 'content-type': 'application/json' } }) } };
73
+ }
74
+ const privateFields = /^(?:token|access[_-]?token|refresh[_-]?token|id[_-]?token|authorization|proxy[_-]?authorization|dpop|poppy[_-]?session|private[_-]?key|client[_-]?secret|client[_-]?assertion|code[_-]?verifier|claim[_-]?secret|callbackRef)$/i;
75
+ export function privateValues(state, includeOAuthState = true) {
76
+ const values = [];
77
+ for (const grant of Object.values(state.grants))
78
+ values.push(grant.accessToken, grant.refreshToken, grant.dpopKey?.handle);
79
+ for (const subject of Object.values(state.subjects))
80
+ values.push(subject.userId, subject.agentId);
81
+ for (const session of Object.values(state.sessions))
82
+ values.push(session.sid);
83
+ for (const delegation of Object.values(state.delegations))
84
+ values.push(delegation.compact, delegation.key.handle);
85
+ for (const tx of Object.values(state.transactions)) {
86
+ values.push(includeOAuthState ? tx.state : undefined, tx.verifier, tx.callbackRef, tx.dpopKey?.handle);
87
+ if (tx.callbackRef)
88
+ try {
89
+ const claim = JSON.parse(tx.callbackRef);
90
+ if (typeof claim.claimSecret === 'string')
91
+ values.push(claim.claimSecret);
92
+ }
93
+ catch { /* No cleartext fallback for an unknown callback reference. */ }
94
+ }
95
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
96
+ }
97
+ export function sanitizePublicValue(value, secrets) { return redact(value, secrets); }
98
+ /** Only business JSON crosses the tool boundary. Protocol headers and credential-shaped fields do not. */
99
+ function redact(value, secrets, depth = 0) {
100
+ requirePap(depth <= 32, 'unsupported_profile');
101
+ const sensitive = (text) => secrets.some(secret => text.includes(secret)) || /(?:^|\s)(?:Bearer|DPoP)\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/.test(text);
102
+ if (typeof value === 'string' && sensitive(value))
103
+ return '[redacted]';
104
+ if (Array.isArray(value))
105
+ return value.map(item => redact(item, secrets, depth + 1));
106
+ if (value && typeof value === 'object')
107
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [sensitive(key) ? '[redacted]' : key, privateFields.test(key) ? '[redacted]' : redact(item, secrets, depth + 1)]));
108
+ return value;
109
+ }
110
+ export function publicResponse(response, secrets = []) {
111
+ const type = response.headers['content-type']?.split(';')[0]?.trim().toLowerCase();
112
+ if (type !== 'application/json' && type !== 'application/problem+json')
113
+ return { status: response.status, bodyAvailable: false };
114
+ try {
115
+ return { status: response.status, body: redact(strictJson(response.body, 65536), secrets) };
116
+ }
117
+ catch {
118
+ return { status: response.status, bodyAvailable: false };
119
+ }
120
+ }
@@ -0,0 +1,90 @@
1
+ import type { PapIdentityHost, PapSigningLease } from './host.js';
2
+ import type { Binding, Company, PublicJwk } from './types.js';
3
+ /** Structural subset of C's real ./protocol-client API. No native or generic signer fallback. */
4
+ export interface ProtocolKeyReference {
5
+ keyRef: object;
6
+ handle: string;
7
+ purpose: 'pap_session' | 'pap_dpop';
8
+ generation: number;
9
+ publicJwk: PublicJwk;
10
+ thumbprint: string;
11
+ }
12
+ export interface ProtocolSignerLease {
13
+ readonly targetRef: object;
14
+ getOAuthClientIdentity(): Promise<{
15
+ clientId: string;
16
+ keyId: string;
17
+ algorithm: 'ES256';
18
+ publicJwk: PublicJwk;
19
+ redirectUris?: string[];
20
+ }>;
21
+ assertActive(): Promise<void>;
22
+ signOAuthClientAssertion(targetRef: object): Promise<string>;
23
+ bindPapScope(input: {
24
+ userId: string;
25
+ accountSlot?: string;
26
+ }): object;
27
+ createDelegatedKey(scopeRef: object, operationId: string): Promise<ProtocolKeyReference>;
28
+ createDpopKey(scopeRef: object, operationId: string): Promise<ProtocolKeyReference>;
29
+ restorePapKey(scopeRef: object, input: {
30
+ handle: string;
31
+ generation: number;
32
+ purpose: 'pap_session' | 'pap_dpop';
33
+ }): Promise<ProtocolKeyReference>;
34
+ issuePapDelegation(scopeRef: object, input: {
35
+ keyRef: object;
36
+ ttlSeconds: number;
37
+ }): Promise<string>;
38
+ signPapSessionRequest(scopeRef: object, input: {
39
+ keyRef: object;
40
+ delegation: string;
41
+ clientId: string;
42
+ sid: string;
43
+ sub: string;
44
+ agentId?: string;
45
+ httpMethod: string;
46
+ httpUri: string;
47
+ accessToken?: string;
48
+ }): Promise<string>;
49
+ signDpopProof(scopeRef: object, input: {
50
+ keyRef: object;
51
+ httpMethod: string;
52
+ httpUri: string;
53
+ accessToken?: string;
54
+ nonce?: string;
55
+ }): Promise<string>;
56
+ retireTemporaryKey(scopeRef: object, keyRef: object): Promise<void>;
57
+ dispose(): void;
58
+ }
59
+ export interface ProtocolIdentityClient {
60
+ registerValidatedTarget(input: {
61
+ identityRef: Binding['identityRef'];
62
+ policyRef: string;
63
+ discovery: {
64
+ profile: 'pap_session' | 'pap_account';
65
+ issuer: string;
66
+ resource: string;
67
+ tokenEndpoint?: string;
68
+ revocationEndpoint?: string;
69
+ companyId: string;
70
+ metadataDigest: string;
71
+ expiresAt: number;
72
+ allowedEndpoints?: string[];
73
+ sources: {
74
+ resource: string;
75
+ authorizationServer: string;
76
+ };
77
+ };
78
+ requestedScopes: string[];
79
+ }): Promise<ProtocolSignerLease>;
80
+ }
81
+ /** Bind to C from the installed Host Context; persist only the native handle and public descriptor. */
82
+ export declare class ProtocolIdentityHost implements PapIdentityHost {
83
+ private readonly client;
84
+ private readonly policyRef;
85
+ private readonly rawLeases;
86
+ private readonly leases;
87
+ constructor(client: ProtocolIdentityClient, policyRef: string);
88
+ dispose(): void;
89
+ acquire(binding: Binding, company: Company, purpose: 'pap_session' | 'pap_account', resource: string, destination?: string, requestedScopes?: readonly string[]): Promise<PapSigningLease>;
90
+ }
@@ -0,0 +1,75 @@
1
+ import { requirePap } from './errors.js';
2
+ import { htu } from './util.js';
3
+ const serializable = (key) => ({ handle: key.handle, purpose: key.purpose, generation: key.generation, public_jwk: key.publicJwk, thumbprint: key.thumbprint });
4
+ /** Bind to C from the installed Host Context; persist only the native handle and public descriptor. */
5
+ export class ProtocolIdentityHost {
6
+ client;
7
+ policyRef;
8
+ rawLeases = new Set();
9
+ leases = new Map();
10
+ constructor(client, policyRef) {
11
+ this.client = client;
12
+ this.policyRef = policyRef;
13
+ }
14
+ dispose() { for (const lease of this.rawLeases)
15
+ lease.dispose(); this.rawLeases.clear(); this.leases.clear(); }
16
+ async acquire(binding, company, purpose, resource, destination, requestedScopes = []) {
17
+ const key = JSON.stringify([binding.identityRef, company.ref, company.generation, purpose, resource, destination, [...requestedScopes].sort()]);
18
+ const existing = this.leases.get(key);
19
+ if (existing) {
20
+ await existing.assertActive();
21
+ return existing;
22
+ }
23
+ const discoveryUrl = company.sources[0].url;
24
+ const raw = await this.client.registerValidatedTarget({ identityRef: binding.identityRef, policyRef: this.policyRef, discovery: {
25
+ profile: purpose, issuer: company.issuer ?? company.companyId, companyId: company.companyId, resource,
26
+ tokenEndpoint: company.metadata?.token_endpoint, revocationEndpoint: company.metadata?.revocation_endpoint,
27
+ metadataDigest: company.metadataDigest, expiresAt: company.expiresAt,
28
+ // Dynamic path parameters are authorized by registering the exact operation URL below.
29
+ allowedEndpoints: [...company.routes.filter(x => !x.template.includes('{')).map(x => htu(x.template)), ...(destination ? [htu(destination)] : [])],
30
+ sources: { resource: discoveryUrl, authorizationServer: company.sources.find(x => x.url.includes('/.well-known/oauth-authorization-server'))?.url ?? discoveryUrl },
31
+ }, requestedScopes: [...requestedScopes] });
32
+ this.rawLeases.add(raw);
33
+ const scopes = new Map();
34
+ const keys = new Map();
35
+ const scopeRef = (scope) => {
36
+ requirePap(scope.company_id === company.companyId && scope.owner === binding.ownerRef, 'permission_denied');
37
+ const cacheKey = JSON.stringify(scope);
38
+ let ref = scopes.get(cacheKey);
39
+ if (!ref) {
40
+ ref = raw.bindPapScope({ userId: scope.user_id, accountSlot: scope.account_slot });
41
+ scopes.set(cacheKey, ref);
42
+ }
43
+ return ref;
44
+ };
45
+ const restored = async (scope, key) => {
46
+ const cacheKey = JSON.stringify([scope, key.handle, key.generation]);
47
+ let ref = keys.get(cacheKey);
48
+ if (!ref) {
49
+ ref = await raw.restorePapKey(scopeRef(scope), { handle: key.handle, generation: key.generation, purpose: key.purpose });
50
+ keys.set(cacheKey, ref);
51
+ }
52
+ requirePap(ref.thumbprint === key.thumbprint && ref.purpose === key.purpose && ref.generation === key.generation, 'credential_unavailable');
53
+ return ref;
54
+ };
55
+ const create = async (scope, operationId, purpose) => {
56
+ const key = await (purpose === 'pap_session' ? raw.createDelegatedKey(scopeRef(scope), operationId) : raw.createDpopKey(scopeRef(scope), operationId));
57
+ keys.set(JSON.stringify([scope, key.handle, key.generation]), key);
58
+ return serializable(key);
59
+ };
60
+ const lease = {
61
+ targetRef: raw.targetRef,
62
+ getOAuthClientIdentity: async () => { const identity = await raw.getOAuthClientIdentity(); return { clientId: identity.clientId, alg: identity.algorithm, kid: identity.keyId, publicJwk: identity.publicJwk, redirectUris: identity.redirectUris }; },
63
+ assertActive: () => raw.assertActive(),
64
+ createDelegatedKey: (scope, operationId) => create(scope, operationId, 'pap_session'),
65
+ createDpopKey: (scope, operationId) => create(scope, operationId, 'pap_dpop'),
66
+ issuePapDelegation: async (key, scope, ttlSeconds) => raw.issuePapDelegation(scopeRef(scope), { keyRef: (await restored(scope, key)).keyRef, ttlSeconds }),
67
+ signPapSessionRequest: async (input) => { const { key, scope, ...request } = input; return raw.signPapSessionRequest(scopeRef(scope), { ...request, keyRef: (await restored(scope, key)).keyRef }); },
68
+ signDpopProof: async (input) => { const { key, scope, ...request } = input; return raw.signDpopProof(scopeRef(scope), { ...request, keyRef: (await restored(scope, key)).keyRef }); },
69
+ signOAuthClientAssertion: () => raw.signOAuthClientAssertion(raw.targetRef),
70
+ retireTemporaryKey: async (key, scope) => raw.retireTemporaryKey(scopeRef(scope), (await restored(scope, key)).keyRef),
71
+ };
72
+ this.leases.set(key, lease);
73
+ return lease;
74
+ }
75
+ }
@@ -0,0 +1,3 @@
1
+ import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
2
+ declare const contribution: TypertRemoteContribution;
3
+ export default contribution;
package/lib/remote.js ADDED
@@ -0,0 +1,17 @@
1
+ /** User-only Typert contract. It is not included in model tools. */
2
+ import { z } from 'zod';
3
+ import { managementRequestSchema } from './product-api.js';
4
+ const result = z.discriminatedUnion('ok', [
5
+ z.object({ ok: z.literal(true), value: z.json().nullable() }).strict(),
6
+ z.object({ ok: z.literal(false), error: z.object({ code: z.string(), retryAfterSeconds: z.union([z.number(), z.string()]).optional() }).strict() }).strict(),
7
+ ]);
8
+ const methods = ['status', 'operations', 'manage'];
9
+ const contribution = {
10
+ package: '@awiki/dsh-pap',
11
+ descriptors: methods.map(method => ({
12
+ id: `@awiki/dsh-pap#papManager/${method}`, service: 'papManager', namespace: 'papManager', method, invocation: { kind: 'direct' },
13
+ parameters: method === 'manage' ? [{ name: 'input', wire: 'input', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-pap/manager#Request', create: () => managementRequestSchema } }] : [],
14
+ result: { mode: 'strict', typeSymbol: 'dsh-pap/manager#Result', create: () => result },
15
+ })),
16
+ };
17
+ export default contribution;
package/lib/store.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { HostSecretStore } from './host.js';
2
+ import type { PapState } from './types.js';
3
+ /** Encrypted, cross-process serialized transactions. The master key stays in Host secret storage. */
4
+ export declare class PapStore {
5
+ readonly root: string;
6
+ private readonly secrets;
7
+ private readonly namespace;
8
+ private readonly path;
9
+ constructor(root: string, secrets: HostSecretStore, namespace: string);
10
+ private key;
11
+ transaction<T>(fn: (state: PapState) => T | Promise<T>): Promise<T>;
12
+ read(): Promise<PapState>;
13
+ recover(): Promise<void>;
14
+ }
package/lib/store.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
+ import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import lockfile from 'proper-lockfile';
5
+ import { PapError, requirePap } from './errors.js';
6
+ import { syncDirectory } from './file-secrets.js';
7
+ import { ensureCredentialVault } from './credential-history.js';
8
+ const empty = () => ({ schemaVersion: 1, generation: 0, companies: {}, subjects: {}, sessions: {}, delegations: {}, grants: {}, transactions: {}, nonces: {}, cooldowns: {}, retirements: {}, accountSlots: {}, delegationAttempts: {}, credentialVault: { version: 1, completeness: 'complete', entries: {}, blocked: false } });
9
+ /** Encrypted, cross-process serialized transactions. The master key stays in Host secret storage. */
10
+ export class PapStore {
11
+ root;
12
+ secrets;
13
+ namespace;
14
+ path;
15
+ constructor(root, secrets, namespace) {
16
+ this.root = root;
17
+ this.secrets = secrets;
18
+ this.namespace = namespace;
19
+ this.path = join(root, 'pap-state.enc');
20
+ }
21
+ async key(existing) {
22
+ try {
23
+ const material = existing ? await this.secrets.getEncryptionKey(`pap/${this.namespace}`) : await this.secrets.getOrCreateEncryptionKey(`pap/${this.namespace}`);
24
+ requirePap(material, 'credential_unavailable');
25
+ const key = Buffer.from(material);
26
+ requirePap(key.length === 32, 'credential_unavailable');
27
+ return key;
28
+ }
29
+ catch {
30
+ throw new PapError('credential_unavailable');
31
+ }
32
+ }
33
+ async transaction(fn) {
34
+ await mkdir(this.root, { recursive: true, mode: 0o700 });
35
+ const release = await lockfile.lock(this.root, { realpath: true, stale: 30000, update: 5000, retries: { retries: 80, minTimeout: 10, maxTimeout: 100 } });
36
+ let key;
37
+ try {
38
+ let bytes;
39
+ try {
40
+ bytes = await readFile(this.path);
41
+ }
42
+ catch (error) {
43
+ if (error.code !== 'ENOENT')
44
+ throw new PapError('state_corrupt');
45
+ }
46
+ key = await this.key(bytes !== undefined);
47
+ let state;
48
+ try {
49
+ if (!bytes)
50
+ state = empty();
51
+ else {
52
+ requirePap(bytes.length > 33 && bytes.subarray(0, 5).toString() === 'PAP01', 'state_corrupt');
53
+ const decipher = createDecipheriv('aes-256-gcm', key, bytes.subarray(5, 17));
54
+ decipher.setAAD(Buffer.from(`pap-state/1/${this.namespace}`));
55
+ decipher.setAuthTag(bytes.subarray(17, 33));
56
+ state = JSON.parse(Buffer.concat([decipher.update(bytes.subarray(33)), decipher.final()]).toString());
57
+ requirePap(state.schemaVersion === 1 && Number.isSafeInteger(state.generation) && state.companies && state.subjects && state.sessions && state.delegations && state.grants && state.transactions && state.nonces && state.retirements, 'state_corrupt');
58
+ // Additive migration of the first private candidate; closed slots stay closed.
59
+ state.accountSlots ??= {};
60
+ state.delegationAttempts ??= {};
61
+ state.cooldowns ??= {};
62
+ for (const grant of Object.values(state.grants)) {
63
+ grant.accountEpoch ??= 1;
64
+ const slot = state.accountSlots[grant.accountSlot] ??= { subjectRef: grant.subjectRef, epoch: grant.accountEpoch, signedOut: false };
65
+ if (['signed_out', 'revocation_pending'].includes(grant.status))
66
+ slot.signedOut = true;
67
+ }
68
+ for (const tx of Object.values(state.transactions)) {
69
+ tx.accountEpoch ??= 1;
70
+ const slot = state.accountSlots[tx.accountSlot] ??= { subjectRef: tx.subjectRef, epoch: tx.accountEpoch, signedOut: false };
71
+ if (slot.signedOut) {
72
+ tx.status = 'cancelled';
73
+ tx.verifier = '';
74
+ delete tx.callbackRef;
75
+ }
76
+ }
77
+ ensureCredentialVault(state);
78
+ }
79
+ }
80
+ catch (error) {
81
+ if (error.code === 'ENOENT')
82
+ state = empty();
83
+ else
84
+ throw new PapError('state_corrupt');
85
+ }
86
+ const result = await fn(state);
87
+ state.generation++;
88
+ const iv = randomBytes(12);
89
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
90
+ cipher.setAAD(Buffer.from(`pap-state/1/${this.namespace}`));
91
+ const encrypted = Buffer.concat([cipher.update(JSON.stringify(state)), cipher.final()]);
92
+ const temp = join(this.root, `.pap-${randomBytes(12).toString('hex')}.tmp`);
93
+ try {
94
+ const file = await open(temp, 'wx', 0o600);
95
+ try {
96
+ await file.writeFile(Buffer.concat([Buffer.from('PAP01'), iv, cipher.getAuthTag(), encrypted]));
97
+ await file.sync();
98
+ }
99
+ finally {
100
+ await file.close();
101
+ }
102
+ await rename(temp, this.path);
103
+ await syncDirectory(this.root);
104
+ }
105
+ finally {
106
+ await rm(temp, { force: true });
107
+ }
108
+ return structuredClone(result);
109
+ }
110
+ finally {
111
+ key?.fill(0);
112
+ await release();
113
+ }
114
+ }
115
+ async read() { return this.transaction(state => state); }
116
+ async recover() {
117
+ await this.transaction(state => {
118
+ for (const grant of Object.values(state.grants))
119
+ if (grant.status === 'refreshing') {
120
+ grant.status = 'uncertain';
121
+ grant.generation++;
122
+ }
123
+ for (const tx of Object.values(state.transactions))
124
+ if (tx.status === 'exchanging')
125
+ tx.status = 'failed';
126
+ });
127
+ }
128
+ }
@@ -0,0 +1,2 @@
1
+ /** Bounded RFC 8259 parser: fatal UTF-8 and duplicate object member rejection. */
2
+ export declare function strictJson(bytes: Uint8Array, maxBytes?: number): unknown;
@@ -0,0 +1,111 @@
1
+ /** Bounded RFC 8259 parser: fatal UTF-8 and duplicate object member rejection. */
2
+ export function strictJson(bytes, maxBytes = 1024 * 1024) {
3
+ if (bytes.length > maxBytes)
4
+ throw new Error('Invalid JSON');
5
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
6
+ let at = 0;
7
+ let nodes = 0;
8
+ const fail = () => { throw new Error('Invalid JSON'); };
9
+ const ws = () => { while (at < text.length && /[\x20\t\r\n]/.test(text[at]))
10
+ at++; };
11
+ const string = () => {
12
+ if (text[at] !== '"')
13
+ return fail();
14
+ const start = at++;
15
+ for (;;) {
16
+ if (at >= text.length)
17
+ return fail();
18
+ const char = text.charCodeAt(at++);
19
+ if (char === 0x22) {
20
+ // JSON.parse validates string escapes and control characters, after token boundaries are fixed.
21
+ return JSON.parse(text.slice(start, at));
22
+ }
23
+ if (char < 0x20)
24
+ return fail();
25
+ if (char === 0x5c) {
26
+ if (at >= text.length)
27
+ return fail();
28
+ const escape = text[at++];
29
+ if (escape === 'u') {
30
+ if (!/^[a-fA-F0-9]{4}$/.test(text.slice(at, at + 4)))
31
+ return fail();
32
+ at += 4;
33
+ }
34
+ else if (!'"\\/bfnrt'.includes(escape))
35
+ return fail();
36
+ }
37
+ }
38
+ };
39
+ const value = (depth) => {
40
+ if (++nodes > 65536 || depth > 32)
41
+ return fail();
42
+ ws();
43
+ const char = text[at];
44
+ if (char === '"')
45
+ return string();
46
+ if (char === '{') {
47
+ at++;
48
+ ws();
49
+ const result = Object.create(null);
50
+ const seen = new Set();
51
+ if (text[at] === '}') {
52
+ at++;
53
+ return result;
54
+ }
55
+ for (;;) {
56
+ ws();
57
+ const key = string();
58
+ if (seen.has(key))
59
+ return fail();
60
+ seen.add(key);
61
+ ws();
62
+ if (text[at++] !== ':')
63
+ return fail();
64
+ result[key] = value(depth + 1);
65
+ ws();
66
+ const end = text[at++];
67
+ if (end === '}')
68
+ return result;
69
+ if (end !== ',')
70
+ return fail();
71
+ }
72
+ }
73
+ if (char === '[') {
74
+ at++;
75
+ ws();
76
+ const result = [];
77
+ if (text[at] === ']') {
78
+ at++;
79
+ return result;
80
+ }
81
+ for (;;) {
82
+ result.push(value(depth + 1));
83
+ ws();
84
+ const end = text[at++];
85
+ if (end === ']')
86
+ return result;
87
+ if (end !== ',')
88
+ return fail();
89
+ }
90
+ }
91
+ for (const [literal, parsed] of [['true', true], ['false', false], ['null', null]]) {
92
+ if (text.startsWith(literal, at)) {
93
+ at += literal.length;
94
+ return parsed;
95
+ }
96
+ }
97
+ const number = text.slice(at).match(/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/);
98
+ if (!number)
99
+ return fail();
100
+ at += number[0].length;
101
+ const parsed = Number(number[0]);
102
+ if (!Number.isFinite(parsed))
103
+ return fail();
104
+ return parsed;
105
+ };
106
+ const parsed = value(0);
107
+ ws();
108
+ if (at !== text.length)
109
+ return fail();
110
+ return parsed;
111
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "@awiki/dsh-pap/tools";
3
+ export declare const inject: string[];
4
+ /** No tools for identity selection, discovery, browser sign-in, callback collection or sign-out. */
5
+ export declare function apply(ctx: Context): void;
package/lib/tools.js ADDED
@@ -0,0 +1,29 @@
1
+ import { PAP_TOOLS } from './product-api.js';
2
+ import { requirePap } from './errors.js';
3
+ export const name = PAP_TOOLS;
4
+ export const inject = ['pap', 'tools'];
5
+ const output = {
6
+ schema: { type: 'object', additionalProperties: true },
7
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
8
+ };
9
+ /** No tools for identity selection, discovery, browser sign-in, callback collection or sign-out. */
10
+ export function apply(ctx) {
11
+ const facade = ctx.pap.acquireTools(ctx);
12
+ const registry = ctx.get('tools');
13
+ requirePap(typeof registry?.register === 'function', 'permission_denied');
14
+ const register = (tool) => { registry.register(tool); };
15
+ const empty = { type: 'object', properties: {}, additionalProperties: false };
16
+ const agent = (execution) => requirePap(execution.agent && !execution.signal?.aborted, 'permission_denied');
17
+ register({ name: 'pap_status', description: 'Read PAP connection and account status. Ask the user to configure PAP or sign in when required.', parameters: empty, output,
18
+ execute: async (_args, execution) => { agent(execution); return facade.status(); } });
19
+ register({ name: 'pap_operations', description: 'List Company operations explicitly enabled by the user. Use their opaque operationRef with pap_request.', parameters: empty, output,
20
+ execute: async (_args, execution) => { agent(execution); return facade.operations(); } });
21
+ register({ name: 'pap_request', description: 'Invoke one user-enabled Company operation. Supply its operationRef and only declared path/query parameters and optional JSON body. Account sign-in is a separate user action.',
22
+ parameters: { type: 'object', additionalProperties: false, required: ['operationRef'], properties: {
23
+ operationRef: { type: 'string' },
24
+ path: { type: 'object', additionalProperties: true },
25
+ query: { type: 'object', additionalProperties: true },
26
+ body: {},
27
+ } }, output,
28
+ execute: async (args, execution) => { agent(execution); return facade.request(args, execution.signal); } });
29
+ }