@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,75 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ import type { HttpTransport, IdentityRef } from './types.js';
3
+ import type { HostSecretStore } from './host.js';
4
+ import { type ProductResult } from './product-api.js';
5
+ export interface PapPluginConfig {
6
+ /** Explicit opt-in; loading an unconfigured bundle performs no discovery or signing. */
7
+ enabled?: boolean;
8
+ stateRoot?: string;
9
+ keyringService?: string;
10
+ /** Explicit local storage selection; no automatic fallback between backends. */
11
+ secretStore?: 'keyring' | 'local-file';
12
+ /** Absolute key directory, separate from stateRoot. Required for local-file. */
13
+ secretsRoot?: string;
14
+ /** Approved C policy chosen by Host configuration, never by a Remote or model. */
15
+ policyRef?: string;
16
+ identityRef?: IdentityRef;
17
+ }
18
+ /** Optional OS-specific Host provider. It never supplies Agent keys or device credentials. */
19
+ export interface PapHostServices {
20
+ secrets: HostSecretStore;
21
+ transport?: HttpTransport;
22
+ }
23
+ declare module '@deepseek-ai/cordis' {
24
+ interface Context {
25
+ pap: PapService;
26
+ papHost: PapHostServices;
27
+ }
28
+ }
29
+ export declare const name = "@awiki/dsh-pap/plugin";
30
+ export declare const inject: string[];
31
+ /** Arbitrary provider/network descriptions never cross a public boundary. */
32
+ export declare function publicResult(operation: () => Promise<unknown>): Promise<ProductResult>;
33
+ /** Actual installed F Host. Engine, C targets and callback capabilities remain in this process. */
34
+ export declare class PapService extends Service {
35
+ private readonly config;
36
+ private readonly installedContext;
37
+ private client?;
38
+ private hostConsumer?;
39
+ private selected?;
40
+ private binding?;
41
+ private stopped;
42
+ private lastError?;
43
+ private readonly ready;
44
+ private readonly authHints;
45
+ private readonly credentialHistory;
46
+ private changing;
47
+ constructor(ctx: Context, config?: PapPluginConfig);
48
+ private assertInstalled;
49
+ private consumer;
50
+ private restore;
51
+ private persistSelection;
52
+ private select;
53
+ private engine;
54
+ private runPublic;
55
+ private publicValue;
56
+ private assertCurrentEngine;
57
+ private catalog;
58
+ start(): Promise<void>;
59
+ stop(): void;
60
+ getStatus(includeManagementHints?: boolean): Promise<object>;
61
+ /** The installed user-only Typert manager receives the management facade. */
62
+ acquireManagement(context: Context): {
63
+ status(): Promise<ProductResult>;
64
+ operations(): Promise<ProductResult>;
65
+ execute(input: unknown): Promise<ProductResult>;
66
+ };
67
+ /** Agent capability contains only approved local operation references. */
68
+ acquireTools(context: Context): {
69
+ status(): Promise<ProductResult>;
70
+ operations(): Promise<ProductResult>;
71
+ request(input: unknown, signal?: AbortSignal): Promise<ProductResult>;
72
+ };
73
+ private manage;
74
+ }
75
+ export declare function apply(ctx: Context, config?: PapPluginConfig): PapService;
package/lib/plugin.js ADDED
@@ -0,0 +1,473 @@
1
+ import { Service } from '@deepseek-ai/cordis';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
4
+ import { PapClient } from './client.js';
5
+ import { ProtocolIdentityHost } from './protocol-adapter.js';
6
+ import { SafeHttpsTransport } from './network.js';
7
+ import { PapError, requirePap } from './errors.js';
8
+ import { KeyringSecretStore } from './keyring.js';
9
+ import { FileSecretStore, syncDirectory } from './file-secrets.js';
10
+ import { DeviceCallbackHandoff } from './callback-handoff.js';
11
+ import { PAP_CONSUMER, PAP_MANAGER, PAP_TOOLS, identityReferenceSchema, managementRequestSchema, operationRequestSchema } from './product-api.js';
12
+ import { assertApproval, findOperation, prepareOperation, privateValues, publicOperations, publicResponse, sanitizePublicValue } from './product-operations.js';
13
+ import { httpsUrl, id, sha256 } from './util.js';
14
+ import { strictJson } from './strict-json.js';
15
+ import { CredentialHistory } from './credential-history.js';
16
+ export const name = PAP_CONSUMER;
17
+ export const inject = ['anpIdentity'];
18
+ const fiberOf = (fiber) => fiber?.ctx?.fiber ?? fiber;
19
+ /** Arbitrary provider/network descriptions never cross a public boundary. */
20
+ export async function publicResult(operation) {
21
+ try {
22
+ return { ok: true, value: await operation() };
23
+ }
24
+ catch (error) {
25
+ if (error instanceof PapError)
26
+ return { ok: false, error: { code: error.code, ...(error.retryAfterSeconds === undefined ? {} : { retryAfterSeconds: error.retryAfterSeconds }) } };
27
+ const code = error?.code;
28
+ if (typeof code === 'string' && ['callback_not_ready', 'callback_claimed', 'callback_expired', 'callback_conflict', 'callback_unavailable', 'callback_limit', 'management_auth_required', 'not_published', 'client_id_conflict', 'idempotency_conflict', 'provider_unavailable', 'provider_disposed', 'consumer_forbidden'].includes(code))
29
+ return { ok: false, error: { code } };
30
+ return { ok: false, error: { code: 'permission_denied' } };
31
+ }
32
+ }
33
+ /** Actual installed F Host. Engine, C targets and callback capabilities remain in this process. */
34
+ export class PapService extends Service {
35
+ config;
36
+ installedContext;
37
+ client;
38
+ hostConsumer;
39
+ selected;
40
+ binding;
41
+ stopped = false;
42
+ lastError;
43
+ ready;
44
+ authHints = new Map();
45
+ credentialHistory = new CredentialHistory();
46
+ changing = false;
47
+ constructor(ctx, config = {}) {
48
+ super(ctx, 'pap');
49
+ this.config = config;
50
+ // Cordis service access can associate this.ctx with a caller. C must always
51
+ // receive the original installed F Context, never the manager/tool caller.
52
+ this.installedContext = ctx;
53
+ ctx.effect(() => () => this.stop());
54
+ this.ready = Promise.resolve().then(() => this.restore());
55
+ }
56
+ assertInstalled(context, expected) {
57
+ requirePap(!this.stopped, 'disconnected');
58
+ const loader = this.installedContext.get('loader');
59
+ const entry = loader?.entries && [...loader.entries()].find(value => fiberOf(value.fiber) === fiberOf(context.fiber));
60
+ requirePap(entry && !entry.disabled && entry.options.name === expected && entry.fiber && [1, 2].includes(entry.fiber.state), 'permission_denied');
61
+ }
62
+ async consumer() {
63
+ requirePap(!this.stopped, 'disconnected');
64
+ requirePap(this.config.enabled === true && typeof this.config.stateRoot === 'string' && isAbsolute(this.config.stateRoot) && Boolean(this.config.policyRef), 'not_configured');
65
+ if (!this.hostConsumer) {
66
+ const identity = this.installedContext.get('anpIdentity');
67
+ requirePap(typeof identity?.bindPapHostConsumer === 'function', 'permission_denied');
68
+ this.hostConsumer = identity.bindPapHostConsumer(this.installedContext);
69
+ }
70
+ await this.hostConsumer.assertActive();
71
+ return this.hostConsumer;
72
+ }
73
+ async restore() {
74
+ if (this.config.enabled !== true)
75
+ return;
76
+ if (typeof this.config.stateRoot !== 'string' || !isAbsolute(this.config.stateRoot) || !this.config.policyRef) {
77
+ this.lastError = 'not_configured';
78
+ return;
79
+ }
80
+ try {
81
+ let reference = this.config.identityRef;
82
+ try {
83
+ reference = identityReferenceSchema.parse(strictJson(await readFile(join(this.config.stateRoot, 'selected-identity.json')), 4096));
84
+ }
85
+ catch (error) {
86
+ if (error.code !== 'ENOENT')
87
+ throw new PapError('state_corrupt');
88
+ }
89
+ if (reference)
90
+ await this.select(reference, false);
91
+ }
92
+ catch (error) {
93
+ this.lastError = error instanceof PapError ? error.code : 'permission_denied';
94
+ }
95
+ }
96
+ async persistSelection(reference) {
97
+ const root = this.config.stateRoot;
98
+ await mkdir(root, { recursive: true, mode: 0o700 });
99
+ const temporary = join(root, `.${id('selection')}.tmp`);
100
+ try {
101
+ const file = await open(temporary, 'wx', 0o600);
102
+ try {
103
+ await file.writeFile(JSON.stringify(reference));
104
+ await file.sync();
105
+ }
106
+ finally {
107
+ await file.close();
108
+ }
109
+ await rename(temporary, join(root, 'selected-identity.json'));
110
+ await syncDirectory(root);
111
+ }
112
+ finally {
113
+ await rm(temporary, { force: true });
114
+ }
115
+ }
116
+ async select(reference, persist) {
117
+ requirePap(!this.changing && !this.stopped, 'state_conflict');
118
+ this.changing = true;
119
+ let next;
120
+ try {
121
+ // A disposed C facade never revives. Only an explicit user selection (or
122
+ // plugin reload) reacquires current authority from the installed Context.
123
+ if (persist) {
124
+ this.hostConsumer?.dispose();
125
+ this.hostConsumer = undefined;
126
+ }
127
+ const consumer = await this.consumer();
128
+ const resolved = await consumer.resolveBinding(reference, this.config.policyRef, 'pap_session');
129
+ const identityRef = resolved.binding.identityRef;
130
+ requirePap(identityRef.storeId === reference.storeId && identityRef.identityId === reference.identityId && identityRef.did === reference.did && typeof resolved.binding.ownerRef === 'string' && resolved.binding.ownerRef.length > 0, 'permission_denied');
131
+ const binding = { identityRef: { storeId: identityRef.storeId, identityId: identityRef.identityId, did: identityRef.did }, ownerRef: resolved.binding.ownerRef, clientId: resolved.binding.clientId };
132
+ httpsUrl(binding.clientId);
133
+ const identity = this.installedContext.get('anpIdentity');
134
+ requirePap(typeof identity.bindProtocolConsumer === 'function', 'permission_denied');
135
+ const protocolHost = new ProtocolIdentityHost(identity.bindProtocolConsumer(this.installedContext), this.config.policyRef);
136
+ const host = this.installedContext.get('papHost');
137
+ requirePap(this.config.secretStore === undefined || ['keyring', 'local-file'].includes(this.config.secretStore), 'not_configured');
138
+ let protectedSecrets;
139
+ if (this.config.secretStore === 'local-file') {
140
+ requirePap(typeof this.config.secretsRoot === 'string' && isAbsolute(this.config.secretsRoot), 'not_configured');
141
+ protectedSecrets = new FileSecretStore(this.config.secretsRoot);
142
+ }
143
+ else if (this.config.secretStore === 'keyring') {
144
+ protectedSecrets = new KeyringSecretStore(this.config.keyringService ?? 'org.agentconnect.dsh-pap', join(this.config.stateRoot, 'keyring-locks'));
145
+ }
146
+ else {
147
+ protectedSecrets = host?.secrets ?? new KeyringSecretStore(this.config.keyringService ?? 'org.agentconnect.dsh-pap', join(this.config.stateRoot, 'keyring-locks'));
148
+ }
149
+ if (protectedSecrets instanceof FileSecretStore)
150
+ protectedSecrets.assertSeparateFrom(this.config.stateRoot);
151
+ const profileNamespace = `dsh-pap/profile/${sha256(resolve(this.config.stateRoot))}/`;
152
+ const secrets = {
153
+ getEncryptionKey: namespace => protectedSecrets.getEncryptionKey(profileNamespace + namespace),
154
+ getOrCreateEncryptionKey: namespace => protectedSecrets.getOrCreateEncryptionKey(profileNamespace + namespace),
155
+ };
156
+ const management = async (transactionId, callbackRef) => {
157
+ await consumer.assertActive();
158
+ let state = await next.store.read();
159
+ let tx = transactionId ? state.transactions[transactionId] : Object.values(state.transactions).find(value => value.callbackRef === callbackRef);
160
+ requirePap(tx && tx.status === 'pending' && tx.expiresAt > Date.now(), 'invalid_authorization_response');
161
+ let subject = state.subjects[tx.subjectRef];
162
+ let company = subject && state.companies[subject.companyRef];
163
+ requirePap(company && company.issuer === tx.issuer && tx.clientId === binding.clientId, 'permission_denied');
164
+ if (company.expiresAt <= Date.now()) {
165
+ await next.resolvePapCompany(company.domain);
166
+ await consumer.assertActive();
167
+ state = await next.store.read();
168
+ tx = transactionId ? state.transactions[transactionId] : Object.values(state.transactions).find(value => value.callbackRef === callbackRef);
169
+ requirePap(tx && tx.status === 'pending' && tx.expiresAt > Date.now(), 'invalid_authorization_response');
170
+ subject = state.subjects[tx.subjectRef];
171
+ company = subject && state.companies[subject.companyRef];
172
+ requirePap(company && company.issuer === tx.issuer && tx.clientId === binding.clientId, 'permission_denied');
173
+ }
174
+ const latest = Object.values(state.companies).filter(value => value.domain === company.domain).sort((left, right) => right.generation - left.generation)[0];
175
+ requirePap(latest?.companyId === company.companyId && latest.issuer === tx.issuer, 'namespace_changed');
176
+ const target = await protocolHost.acquire(binding, company, 'pap_account', tx.resource, undefined, tx.scope);
177
+ requirePap(target.targetRef, 'permission_denied');
178
+ return consumer.callbackManagementFor({ identityRef: reference, policyRef: this.config.policyRef, targetRef: target.targetRef });
179
+ };
180
+ const handoff = {
181
+ register: async (input) => { const driver = await management(input.transactionId); try {
182
+ return await new DeviceCallbackHandoff(driver).register(input);
183
+ }
184
+ finally {
185
+ driver.dispose();
186
+ } },
187
+ collect: async (callbackRef) => { const driver = await management(undefined, callbackRef); try {
188
+ return await new DeviceCallbackHandoff(driver).collect(callbackRef);
189
+ }
190
+ finally {
191
+ driver.dispose();
192
+ } },
193
+ };
194
+ next = new PapClient({ stateRoot: join(this.config.stateRoot, `identity-${sha256(JSON.stringify(binding))}`), binding, identityHost: protocolHost, secretStore: secrets, callbackHandoff: handoff, observeCredentials: credentials => this.credentialHistory.observe(credentials) }, host?.transport ?? new SafeHttpsTransport());
195
+ await next.recover();
196
+ await consumer.assertActive();
197
+ requirePap(!this.stopped, 'disconnected');
198
+ if (persist)
199
+ await this.persistSelection(reference);
200
+ await consumer.assertActive();
201
+ requirePap(!this.stopped, 'disconnected');
202
+ this.client?.close();
203
+ this.authHints.clear();
204
+ this.client = next;
205
+ this.binding = binding;
206
+ this.selected = structuredClone(reference);
207
+ this.lastError = undefined;
208
+ }
209
+ catch (error) {
210
+ next?.close();
211
+ this.lastError = error instanceof PapError ? error.code : 'permission_denied';
212
+ throw error;
213
+ }
214
+ finally {
215
+ this.changing = false;
216
+ }
217
+ }
218
+ async engine() {
219
+ await this.ready;
220
+ requirePap(!this.stopped, 'disconnected');
221
+ requirePap(this.client, 'not_configured');
222
+ await (await this.consumer()).assertActive();
223
+ return this.client;
224
+ }
225
+ async runPublic(operation) {
226
+ const result = await publicResult(operation);
227
+ if (this.stopped)
228
+ return { ok: false, error: { code: 'disconnected' } };
229
+ try {
230
+ return sanitizePublicValue(result, this.credentialHistory.values());
231
+ }
232
+ catch {
233
+ return { ok: false, error: { code: 'credential_unavailable' } };
234
+ }
235
+ }
236
+ publicValue(state, value, userAuthorizationUrl = false) {
237
+ requirePap(state.credentialVault?.completeness === 'complete', 'credential_history_incomplete');
238
+ this.credentialHistory.restore(state);
239
+ const tokens = this.credentialHistory.values();
240
+ const sanitized = sanitizePublicValue(value, [...tokens, ...privateValues(state)]);
241
+ if (userAuthorizationUrl && value && typeof value === 'object' && !Array.isArray(value) && typeof value.authorizationUrl === 'string') {
242
+ // Only this explicit user result may carry the AS-bound state/challenge.
243
+ sanitized.authorizationUrl = sanitizePublicValue(value.authorizationUrl, [...tokens, ...privateValues(state, false)]);
244
+ }
245
+ return sanitized;
246
+ }
247
+ assertCurrentEngine(engine) { requirePap(!this.stopped, 'disconnected'); requirePap(!this.changing && this.client === engine, 'state_conflict'); }
248
+ async catalog(enabledOnly, check) {
249
+ check();
250
+ const engine = await this.engine();
251
+ const state = await engine.store.read();
252
+ check();
253
+ this.assertCurrentEngine(engine);
254
+ await (await this.consumer()).assertActive();
255
+ check();
256
+ this.assertCurrentEngine(engine);
257
+ return this.publicValue(state, publicOperations(state, enabledOnly));
258
+ }
259
+ async start() { await this.ready; }
260
+ stop() { if (this.stopped)
261
+ return; this.stopped = true; this.client?.close(); this.hostConsumer?.dispose(); this.credentialHistory.dispose(); }
262
+ async getStatus(includeManagementHints = false) {
263
+ await this.ready;
264
+ requirePap(!this.stopped, 'disconnected');
265
+ if (this.config.enabled !== true)
266
+ return { state: 'disabled' };
267
+ if (!this.client)
268
+ return { state: this.lastError === 'credential_unavailable' ? 'credential_unavailable' : 'unconfigured', ...(this.lastError ? { error: this.lastError } : {}) };
269
+ const engine = this.client;
270
+ await (await this.consumer()).assertActive();
271
+ const state = await engine.store.read();
272
+ this.assertCurrentEngine(engine);
273
+ await (await this.consumer()).assertActive();
274
+ this.assertCurrentEngine(engine);
275
+ if (state.credentialVault?.completeness !== 'complete' || state.credentialVault.blocked) {
276
+ if (!includeManagementHints)
277
+ throw new PapError(state.credentialVault?.completeness !== 'complete' ? 'credential_history_incomplete' : 'credential_unavailable');
278
+ return { state: state.credentialVault?.completeness !== 'complete' ? 'migration_required' : 'credential_unavailable', error: state.credentialVault?.completeness !== 'complete' ? 'credential_history_incomplete' : 'credential_unavailable',
279
+ accounts: [...new Map([...Object.values(state.grants).map(value => [value.accountSlot, { accountSlot: value.accountSlot, status: value.status, scopes: [] }]), ...Object.values(state.transactions).map(value => [value.accountSlot, { accountSlot: value.accountSlot, status: 'pending_login', scopes: [] }])]).values()] };
280
+ }
281
+ return this.publicValue(state, { state: 'ready', identity: this.selected, supportedSignIn: ['direct'],
282
+ ...(includeManagementHints ? { authenticationHints: [...this.authHints].map(([operationRef, hint]) => ({ operationRef, ...hint })),
283
+ pendingSignIns: Object.values(state.transactions).filter(value => value.status === 'pending' && value.expiresAt > Date.now()).map(value => ({ transactionRef: value.ref, accountSlot: value.accountSlot, ...(state.subjects[value.subjectRef] ? { companyRef: state.subjects[value.subjectRef].companyRef } : {}), expiresAt: value.expiresAt })) } : {}),
284
+ companies: Object.values(state.companies).map(value => ({ companyRef: value.ref, name: value.name, authMode: value.authMode, scopes: value.scopes })),
285
+ sessions: Object.values(state.sessions).map(value => ({ sessionRef: value.ref, ...(state.subjects[value.subjectRef] ? { companyRef: state.subjects[value.subjectRef].companyRef } : {}), connected: value.connected, account: Boolean(value.accountSlot), ...(value.accountSlot ? { accountSlot: value.accountSlot } : {}) })),
286
+ accounts: Object.values(state.grants).map(value => ({ accountSlot: value.accountSlot, ...(state.subjects[value.subjectRef] ? { companyRef: state.subjects[value.subjectRef].companyRef } : {}), status: value.status, scopes: value.scope, canRefresh: value.status === 'active' && Boolean(value.refreshToken) })),
287
+ });
288
+ }
289
+ /** The installed user-only Typert manager receives the management facade. */
290
+ acquireManagement(context) {
291
+ this.assertInstalled(context, PAP_MANAGER);
292
+ const check = () => this.assertInstalled(context, PAP_MANAGER);
293
+ return Object.freeze({
294
+ status: () => this.runPublic(async () => { check(); const result = await this.getStatus(true); check(); return result; }),
295
+ operations: () => this.runPublic(() => this.catalog(false, check)),
296
+ execute: input => this.runPublic(async () => {
297
+ check();
298
+ const parsed = managementRequestSchema.safeParse(input);
299
+ requirePap(parsed.success, 'permission_denied');
300
+ const previous = this.client;
301
+ const result = await this.manage(parsed.data, check);
302
+ check();
303
+ if (parsed.data.action !== 'identity/select' && previous) {
304
+ this.assertCurrentEngine(previous);
305
+ await (await this.consumer()).assertActive();
306
+ check();
307
+ this.assertCurrentEngine(previous);
308
+ }
309
+ const current = this.client;
310
+ if (!current)
311
+ return result;
312
+ const state = await current.store.read();
313
+ check();
314
+ this.assertCurrentEngine(current);
315
+ await (await this.consumer()).assertActive();
316
+ check();
317
+ this.assertCurrentEngine(current);
318
+ if (parsed.data.action === 'account/sign-out' && state.credentialVault?.completeness !== 'complete')
319
+ return result;
320
+ if (parsed.data.action === 'identity/select' && state.credentialVault?.completeness !== 'complete')
321
+ return result;
322
+ return this.publicValue(state, result, parsed.data.action === 'account/sign-in');
323
+ }),
324
+ });
325
+ }
326
+ /** Agent capability contains only approved local operation references. */
327
+ acquireTools(context) {
328
+ this.assertInstalled(context, PAP_TOOLS);
329
+ const check = () => this.assertInstalled(context, PAP_TOOLS);
330
+ return Object.freeze({
331
+ status: () => this.runPublic(async () => { check(); const result = await this.getStatus(); check(); return result; }),
332
+ operations: () => this.runPublic(() => this.catalog(true, check)),
333
+ request: (input, signal) => this.runPublic(async () => {
334
+ check();
335
+ const parsed = operationRequestSchema.safeParse(input);
336
+ requirePap(parsed.success, 'permission_denied');
337
+ const engine = await this.engine();
338
+ const pin = this.credentialHistory.begin();
339
+ try {
340
+ const snapshot = await engine.store.read();
341
+ requirePap(snapshot.credentialVault?.completeness === 'complete', 'credential_history_incomplete');
342
+ const prepared = prepareOperation(snapshot, parsed.data);
343
+ this.credentialHistory.restore(snapshot);
344
+ for (const grant of Object.values(snapshot.grants)) {
345
+ this.credentialHistory.rememberIfAbsent(grant.accessToken);
346
+ if (grant.refreshToken)
347
+ this.credentialHistory.rememberIfAbsent(grant.refreshToken);
348
+ }
349
+ const checkRequest = () => { check(); this.assertCurrentEngine(engine); };
350
+ checkRequest();
351
+ this.credentialHistory.assertUsable();
352
+ const sentSecrets = new Set(privateValues(snapshot));
353
+ try {
354
+ const response = await engine.dispatchPap(prepared.approval.sessionRef, prepared.operation, { signal,
355
+ assertAllowed: state => { checkRequest(); this.credentialHistory.assertUsable(); assertApproval(state, parsed.data.operationRef, prepared.approval); },
356
+ captureSecrets: (values, credentials) => { for (const value of values)
357
+ sentSecrets.add(value); if (credentials)
358
+ this.credentialHistory.observe(credentials); this.credentialHistory.assertUsable(); },
359
+ });
360
+ checkRequest();
361
+ await (await this.consumer()).assertActive();
362
+ checkRequest();
363
+ const current = await engine.store.read();
364
+ assertApproval(current, parsed.data.operationRef, prepared.approval);
365
+ this.credentialHistory.restore(current);
366
+ const secrets = [...sentSecrets, ...privateValues(current), ...this.credentialHistory.values()];
367
+ checkRequest();
368
+ await (await this.consumer()).assertActive();
369
+ checkRequest();
370
+ this.authHints.delete(parsed.data.operationRef);
371
+ return publicResponse(response, secrets);
372
+ }
373
+ catch (error) {
374
+ if (this.client === engine && !this.changing && error instanceof PapError && error.authHint) {
375
+ if (this.authHints.size >= 32)
376
+ this.authHints.delete(this.authHints.keys().next().value);
377
+ this.authHints.set(parsed.data.operationRef, error.authHint);
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+ finally {
383
+ this.credentialHistory.end(pin);
384
+ }
385
+ }),
386
+ });
387
+ }
388
+ async manage(input, check) {
389
+ await this.ready;
390
+ check();
391
+ if (input.action === 'identity/select') {
392
+ await this.select(input.reference, true);
393
+ check();
394
+ return this.getStatus(true);
395
+ }
396
+ const engine = await this.engine();
397
+ check();
398
+ if (input.action !== 'account/sign-out')
399
+ requirePap((await engine.store.read()).credentialVault?.completeness === 'complete', 'credential_history_incomplete');
400
+ if (input.action === 'company/discover')
401
+ return engine.resolvePapCompany(input.domain);
402
+ if (input.action === 'session/connect') {
403
+ const subject = await engine.getOrCreatePapUser(input.companyRef, input.mode ?? 'regular', true);
404
+ check();
405
+ return { ...subject, ...(await engine.openPapSession(subject.subjectRef, input.accountSlot)) };
406
+ }
407
+ if (input.action === 'session/disconnect') {
408
+ await engine.disconnectPap(input.sessionRef);
409
+ return null;
410
+ }
411
+ if (input.action === 'operation/disable') {
412
+ await engine.store.transaction(state => { check(); if (state.product)
413
+ delete state.product.operations[input.operationRef]; });
414
+ return null;
415
+ }
416
+ if (input.action === 'operation/enable')
417
+ return engine.store.transaction(state => {
418
+ check();
419
+ const { company, route } = findOperation(state, input.operationRef);
420
+ const session = state.sessions[input.sessionRef];
421
+ requirePap(session?.connected && state.subjects[session.subjectRef]?.companyRef === company.ref, 'permission_denied');
422
+ const scopes = [...new Set(input.scopes ?? [])].sort();
423
+ requirePap(scopes.every(scope => company.scopes.includes(scope)) && (session.accountSlot || scopes.length === 0), 'insufficient_scope');
424
+ state.product ??= { operations: {} };
425
+ state.product.operations[input.operationRef] = { companyRef: company.ref, method: route.method, template: route.template, resource: route.resource, sessionRef: input.sessionRef, scopes, generation: state.generation + 1 };
426
+ return { operationRef: input.operationRef, enabled: true };
427
+ });
428
+ if (input.action === 'account/sign-in') {
429
+ const state = await engine.store.read();
430
+ const { company, route } = findOperation(state, input.operationRef);
431
+ const session = state.sessions[input.sessionRef];
432
+ requirePap(session?.connected && state.subjects[session.subjectRef]?.companyRef === company.ref, 'permission_denied');
433
+ const resolved = await (await this.consumer()).resolveBinding(this.selected, this.config.policyRef, 'pap_account');
434
+ const current = this.binding;
435
+ const candidate = resolved.binding;
436
+ requirePap(candidate.clientId === current.clientId && candidate.ownerRef === current.ownerRef && candidate.identityRef.storeId === current.identityRef.storeId && candidate.identityRef.identityId === current.identityRef.identityId && candidate.identityRef.did === current.identityRef.did && resolved.redirectUri, 'unsupported_sign_in');
437
+ httpsUrl(resolved.redirectUri);
438
+ check();
439
+ return engine.startSignIn(session.subjectRef, { scopes: input.scopes, resource: route.resource, redirectUri: resolved.redirectUri, callbackMode: 'handoff', method: 'direct' });
440
+ }
441
+ if (input.action === 'account/collect')
442
+ return engine.collectSignIn(input.transactionRef);
443
+ const state = await engine.store.read();
444
+ const account = Object.values(state.grants).find(value => value.accountSlot === input.accountSlot) ?? Object.values(state.transactions).find(value => value.accountSlot === input.accountSlot);
445
+ requirePap(account, 'reauthorization_required');
446
+ if (input.action === 'account/refresh') {
447
+ const grant = state.grants[account.ref];
448
+ requirePap(grant?.status === 'active' && grant.refreshToken, 'reauthorization_required');
449
+ const subject = state.subjects[grant.subjectRef];
450
+ const company = subject && state.companies[subject.companyRef];
451
+ const slot = state.accountSlots[input.accountSlot];
452
+ requirePap(subject && company && company.companyId === subject.companyId && grant.issuer === company.issuer && grant.clientId === this.binding?.clientId && JSON.stringify(subject.binding) === JSON.stringify(this.binding) && company.routes.some(route => route.resource === grant.resource), 'permission_denied');
453
+ requirePap(slot && !slot.signedOut && slot.epoch === grant.accountEpoch, 'reauthorization_required');
454
+ const sessions = Object.values(state.sessions).filter(session => session.connected && session.accountSlot === input.accountSlot).map(session => ({ ref: session.ref, generation: session.generation }));
455
+ check();
456
+ this.assertCurrentEngine(engine);
457
+ await (await this.consumer()).assertActive();
458
+ check();
459
+ await engine.refreshAccountGrant(grant.ref);
460
+ const current = await engine.store.read();
461
+ check();
462
+ this.assertCurrentEngine(engine);
463
+ await (await this.consumer()).assertActive();
464
+ check();
465
+ this.assertCurrentEngine(engine);
466
+ requirePap(current.accountSlots[input.accountSlot]?.epoch === slot.epoch && !current.accountSlots[input.accountSlot]?.signedOut && current.grants[grant.ref]?.status === 'active', 'reauthorization_required');
467
+ requirePap(current.companies[subject.companyRef]?.companyId === company.companyId && current.companies[subject.companyRef]?.issuer === company.issuer && sessions.every(session => current.sessions[session.ref]?.connected && current.sessions[session.ref]?.generation === session.generation && current.sessions[session.ref]?.accountSlot === input.accountSlot), 'state_conflict');
468
+ return { status: 'active' };
469
+ }
470
+ return engine.signOutAccount(account.subjectRef, input.accountSlot);
471
+ }
472
+ }
473
+ export function apply(ctx, config = {}) { return new PapService(ctx, config); }
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ import type { Binding, IdentityRef } from './types.js';
3
+ import type { DeviceManagementOperations } from './callback-handoff.js';
4
+ export declare const PAP_CONSUMER = "@awiki/dsh-pap/plugin";
5
+ export declare const PAP_MANAGER = "@awiki/dsh-pap/management-remote";
6
+ export declare const PAP_TOOLS = "@awiki/dsh-pap/tools";
7
+ /** C-owned capability. The caller is the actual installed F Context, never a wire name. */
8
+ export interface PapHostConsumer {
9
+ resolveBinding(identityRef: IdentityRef, policyRef: string, profile: 'pap_session' | 'pap_account'): Promise<{
10
+ binding: Binding;
11
+ redirectUri?: string;
12
+ }>;
13
+ callbackManagementFor(input: {
14
+ identityRef: IdentityRef;
15
+ policyRef: string;
16
+ targetRef: object;
17
+ }): Promise<DeviceManagementOperations & {
18
+ dispose(): void;
19
+ }>;
20
+ assertActive(): void | Promise<void>;
21
+ dispose(): void;
22
+ }
23
+ export declare const identityReferenceSchema: z.ZodObject<{
24
+ storeId: z.ZodString;
25
+ identityId: z.ZodString;
26
+ did: z.ZodString;
27
+ }, z.core.$strict>;
28
+ export declare const managementRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ action: z.ZodLiteral<"identity/select">;
30
+ reference: z.ZodObject<{
31
+ storeId: z.ZodString;
32
+ identityId: z.ZodString;
33
+ did: z.ZodString;
34
+ }, z.core.$strict>;
35
+ }, z.core.$strict>, z.ZodObject<{
36
+ action: z.ZodLiteral<"company/discover">;
37
+ domain: z.ZodString;
38
+ }, z.core.$strict>, z.ZodObject<{
39
+ action: z.ZodLiteral<"session/connect">;
40
+ companyRef: z.ZodString;
41
+ mode: z.ZodOptional<z.ZodEnum<{
42
+ regular: "regular";
43
+ anonymous: "anonymous";
44
+ }>>;
45
+ accountSlot: z.ZodOptional<z.ZodString>;
46
+ }, z.core.$strict>, z.ZodObject<{
47
+ action: z.ZodLiteral<"session/disconnect">;
48
+ sessionRef: z.ZodString;
49
+ }, z.core.$strict>, z.ZodObject<{
50
+ action: z.ZodLiteral<"operation/enable">;
51
+ operationRef: z.ZodString;
52
+ sessionRef: z.ZodString;
53
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ }, z.core.$strict>, z.ZodObject<{
55
+ action: z.ZodLiteral<"operation/disable">;
56
+ operationRef: z.ZodString;
57
+ }, z.core.$strict>, z.ZodObject<{
58
+ action: z.ZodLiteral<"account/sign-in">;
59
+ sessionRef: z.ZodString;
60
+ operationRef: z.ZodString;
61
+ scopes: z.ZodArray<z.ZodString>;
62
+ }, z.core.$strict>, z.ZodObject<{
63
+ action: z.ZodLiteral<"account/collect">;
64
+ transactionRef: z.ZodString;
65
+ }, z.core.$strict>, z.ZodObject<{
66
+ action: z.ZodLiteral<"account/sign-out">;
67
+ accountSlot: z.ZodString;
68
+ }, z.core.$strict>, z.ZodObject<{
69
+ action: z.ZodLiteral<"account/refresh">;
70
+ accountSlot: z.ZodString;
71
+ }, z.core.$strict>], "action">;
72
+ export type PapManagementRequest = z.infer<typeof managementRequestSchema>;
73
+ export declare const operationRequestSchema: z.ZodObject<{
74
+ operationRef: z.ZodString;
75
+ path: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
76
+ query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
77
+ body: z.ZodOptional<z.ZodJSONSchema>;
78
+ }, z.core.$strict>;
79
+ export type PapOperationRequest = z.infer<typeof operationRequestSchema>;
80
+ export type ProductResult = {
81
+ ok: true;
82
+ value: unknown;
83
+ } | {
84
+ ok: false;
85
+ error: {
86
+ code: string;
87
+ retryAfterSeconds?: number | string;
88
+ };
89
+ };
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ export const PAP_CONSUMER = '@awiki/dsh-pap/plugin';
3
+ export const PAP_MANAGER = '@awiki/dsh-pap/management-remote';
4
+ export const PAP_TOOLS = '@awiki/dsh-pap/tools';
5
+ const ref = z.string().min(1).max(256);
6
+ export const identityReferenceSchema = z.object({ storeId: ref, identityId: ref, did: ref }).strict();
7
+ const scope = z.string().min(1).max(256).regex(/^[\x21\x23-\x5b\x5d-\x7e]+$/);
8
+ export const managementRequestSchema = z.discriminatedUnion('action', [
9
+ z.object({ action: z.literal('identity/select'), reference: identityReferenceSchema }).strict(),
10
+ z.object({ action: z.literal('company/discover'), domain: z.string().min(1).max(253) }).strict(),
11
+ z.object({ action: z.literal('session/connect'), companyRef: ref, mode: z.enum(['regular', 'anonymous']).optional(), accountSlot: ref.optional() }).strict(),
12
+ z.object({ action: z.literal('session/disconnect'), sessionRef: ref }).strict(),
13
+ z.object({ action: z.literal('operation/enable'), operationRef: ref, sessionRef: ref, scopes: z.array(scope).max(64).optional() }).strict(),
14
+ z.object({ action: z.literal('operation/disable'), operationRef: ref }).strict(),
15
+ z.object({ action: z.literal('account/sign-in'), sessionRef: ref, operationRef: ref, scopes: z.array(scope).min(1).max(64) }).strict(),
16
+ z.object({ action: z.literal('account/collect'), transactionRef: ref }).strict(),
17
+ z.object({ action: z.literal('account/sign-out'), accountSlot: ref }).strict(),
18
+ z.object({ action: z.literal('account/refresh'), accountSlot: ref }).strict(),
19
+ ]);
20
+ export const operationRequestSchema = z.object({
21
+ operationRef: ref,
22
+ path: z.record(z.string(), z.string().max(1024)).optional(),
23
+ query: z.record(z.string(), z.string().max(4096)).optional(),
24
+ body: z.json().optional(),
25
+ }).strict();