@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,148 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { chmod, lstat, mkdir, open, link, rm } from 'node:fs/promises';
4
+ import { isAbsolute, join, resolve, relative, sep } from 'node:path';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { PapError, requirePap } from './errors.js';
8
+ const execute = promisify(execFile);
9
+ /** Protect only this user's directory/file; Windows uses DACLs rather than chmod. */
10
+ async function protect(path, directory) {
11
+ const info = await lstat(path);
12
+ requirePap(!info.isSymbolicLink() && (directory ? info.isDirectory() : info.isFile()), 'credential_unavailable');
13
+ if (process.platform !== 'win32') {
14
+ requirePap(typeof process.getuid === 'function' && info.uid === process.getuid(), 'credential_unavailable');
15
+ await chmod(path, directory ? 0o700 : 0o600);
16
+ return;
17
+ }
18
+ // Paths travel in the child environment, never as executable PowerShell text.
19
+ const script = `
20
+ $ErrorActionPreference = 'Stop'
21
+ $p = $env:DSH_PAP_PROTECT_PATH
22
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
23
+ $old = Get-Acl -LiteralPath $p
24
+ if ($old.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Unexpected owner' }
25
+ if ($env:DSH_PAP_PROTECT_DIRECTORY -eq '1') {
26
+ $acl = New-Object System.Security.AccessControl.DirectorySecurity
27
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
28
+ } else {
29
+ $acl = New-Object System.Security.AccessControl.FileSecurity
30
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'Allow')
31
+ }
32
+ $acl.SetOwner($sid)
33
+ $acl.SetAccessRuleProtection($true, $false)
34
+ $acl.AddAccessRule($rule)
35
+ Set-Acl -LiteralPath $p -AclObject $acl
36
+ `;
37
+ await execute('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], {
38
+ env: { ...process.env, DSH_PAP_PROTECT_PATH: path, DSH_PAP_PROTECT_DIRECTORY: directory ? '1' : '0' },
39
+ timeout: 10000, windowsHide: true, maxBuffer: 4096,
40
+ });
41
+ }
42
+ /** Flush directories where the OS supports opening them; Windows still flushes files. */
43
+ export async function syncDirectory(path) {
44
+ if (process.platform === 'win32')
45
+ return;
46
+ const directory = await open(path, 'r');
47
+ try {
48
+ await directory.sync();
49
+ }
50
+ finally {
51
+ await directory.close();
52
+ }
53
+ }
54
+ /** File-backed PAP state encryption keys. Identity/signing keys remain in native custody. */
55
+ export class FileSecretStore {
56
+ root;
57
+ constructor(root) {
58
+ requirePap(isAbsolute(root), 'credential_unavailable');
59
+ this.root = resolve(root);
60
+ }
61
+ assertSeparateFrom(stateRoot) {
62
+ const inside = (parent, child) => {
63
+ const path = relative(resolve(parent), resolve(child));
64
+ return path === '' || (path !== '..' && !path.startsWith('..' + sep) && !isAbsolute(path));
65
+ };
66
+ requirePap(!inside(stateRoot, this.root) && !inside(this.root, stateRoot), 'credential_unavailable');
67
+ }
68
+ path(namespace) {
69
+ requirePap(typeof namespace === 'string' && namespace.length > 0, 'credential_unavailable');
70
+ return join(this.root, createHash('sha256').update(namespace).digest('hex') + '.key');
71
+ }
72
+ async prepare() {
73
+ await mkdir(this.root, { recursive: true, mode: 0o700 });
74
+ await protect(this.root, true);
75
+ }
76
+ async read(path) {
77
+ try {
78
+ await protect(path, false);
79
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
80
+ try {
81
+ const info = await file.stat();
82
+ requirePap(info.isFile() && info.size === 32, 'credential_unavailable');
83
+ const value = await file.readFile();
84
+ requirePap(value.length === 32, 'credential_unavailable');
85
+ return value;
86
+ }
87
+ finally {
88
+ await file.close();
89
+ }
90
+ }
91
+ catch (error) {
92
+ if (error.code === 'ENOENT')
93
+ return undefined;
94
+ throw new PapError('credential_unavailable');
95
+ }
96
+ }
97
+ async getEncryptionKey(namespace) {
98
+ try {
99
+ await this.prepare();
100
+ return await this.read(this.path(namespace));
101
+ }
102
+ catch {
103
+ throw new PapError('credential_unavailable');
104
+ }
105
+ }
106
+ async getOrCreateEncryptionKey(namespace) {
107
+ let temporary;
108
+ let material;
109
+ try {
110
+ await this.prepare();
111
+ const path = this.path(namespace);
112
+ const existing = await this.read(path);
113
+ if (existing)
114
+ return existing;
115
+ temporary = join(this.root, '.' + randomBytes(16).toString('hex') + '.tmp');
116
+ const file = await open(temporary, 'wx', 0o600);
117
+ try {
118
+ await protect(temporary, false);
119
+ material = randomBytes(32);
120
+ await file.writeFile(material);
121
+ await file.sync();
122
+ }
123
+ finally {
124
+ await file.close();
125
+ }
126
+ // A hard link publishes a fully written key atomically without replacing a winner.
127
+ try {
128
+ await link(temporary, path);
129
+ }
130
+ catch (error) {
131
+ if (error.code !== 'EEXIST')
132
+ throw error;
133
+ }
134
+ await syncDirectory(this.root);
135
+ const result = await this.read(path);
136
+ requirePap(result, 'credential_unavailable');
137
+ return result;
138
+ }
139
+ catch {
140
+ throw new PapError('credential_unavailable');
141
+ }
142
+ finally {
143
+ material?.fill(0);
144
+ if (temporary)
145
+ await rm(temporary, { force: true }).catch(() => { throw new PapError('credential_unavailable'); });
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,16 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ import type { HostSecretStore } from './host.js';
3
+ import type { PapHostServices } from './plugin.js';
4
+ export interface Config {
5
+ secretStore?: 'keyring' | 'local-file';
6
+ secretsRoot?: string;
7
+ keyringService?: string;
8
+ locksRoot?: string;
9
+ }
10
+ export declare const name = "@awiki/dsh-pap/host-secrets";
11
+ /** Optional Host secrets provider with an explicitly selected storage backend. */
12
+ export declare class PapHostService extends Service implements PapHostServices {
13
+ readonly secrets: HostSecretStore;
14
+ constructor(ctx: Context, config: Config);
15
+ }
16
+ export declare function apply(ctx: Context, config: Config): PapHostService;
@@ -0,0 +1,22 @@
1
+ import { Service } from '@deepseek-ai/cordis';
2
+ import { KeyringSecretStore } from './keyring.js';
3
+ import { FileSecretStore } from './file-secrets.js';
4
+ import { requirePap } from './errors.js';
5
+ export const name = '@awiki/dsh-pap/host-secrets';
6
+ /** Optional Host secrets provider with an explicitly selected storage backend. */
7
+ export class PapHostService extends Service {
8
+ secrets;
9
+ constructor(ctx, config) {
10
+ super(ctx, 'papHost');
11
+ requirePap(config.secretStore === undefined || ['keyring', 'local-file'].includes(config.secretStore), 'not_configured');
12
+ if (config.secretStore === 'local-file') {
13
+ requirePap(typeof config.secretsRoot === 'string', 'not_configured');
14
+ this.secrets = new FileSecretStore(config.secretsRoot);
15
+ }
16
+ else {
17
+ requirePap(typeof config.keyringService === 'string' && typeof config.locksRoot === 'string', 'not_configured');
18
+ this.secrets = new KeyringSecretStore(config.keyringService, config.locksRoot);
19
+ }
20
+ }
21
+ }
22
+ export function apply(ctx, config) { return new PapHostService(ctx, config); }
package/lib/host.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { AsMetadata, Binding, Company, IdentityRef, KeyRef, KeyScope, PublicJwk } from './types.js';
2
+ import type { CredentialObservation } from './credential-history.js';
3
+ /** Same-process Host capability. Never expose through Remote or tools. */
4
+ export interface PapSigningLease {
5
+ /** Actual C target object, Host-only; never serialized or accepted from user input. */
6
+ readonly targetRef?: object;
7
+ getOAuthClientIdentity(): Promise<{
8
+ clientId: string;
9
+ alg: string;
10
+ kid: string;
11
+ publicJwk?: PublicJwk;
12
+ redirectUris?: string[];
13
+ }>;
14
+ createDelegatedKey(scope: KeyScope, operationId: string): Promise<KeyRef>;
15
+ issuePapDelegation(key: KeyRef, scope: KeyScope, ttlSeconds: number): Promise<string>;
16
+ signPapSessionRequest(input: {
17
+ key: KeyRef;
18
+ scope: KeyScope;
19
+ delegation: string;
20
+ clientId: string;
21
+ sid: string;
22
+ sub: string;
23
+ agentId?: string;
24
+ httpMethod: string;
25
+ httpUri: string;
26
+ accessToken?: string;
27
+ }): Promise<string>;
28
+ createDpopKey(scope: KeyScope, operationId: string): Promise<KeyRef>;
29
+ signDpopProof(input: {
30
+ key: KeyRef;
31
+ scope: KeyScope;
32
+ httpMethod: string;
33
+ httpUri: string;
34
+ accessToken?: string;
35
+ nonce?: string;
36
+ }): Promise<string>;
37
+ signOAuthClientAssertion(): Promise<string>;
38
+ retireTemporaryKey(key: KeyRef, scope: KeyScope): Promise<void>;
39
+ assertActive(): Promise<void>;
40
+ }
41
+ export interface PapIdentityHost {
42
+ dispose?(): void;
43
+ /** Host must independently verify sources and enforce preapproved target policy. */
44
+ acquire(binding: Binding, company: Company, purpose: 'pap_session' | 'pap_account', resource: string, destination?: string, requestedScopes?: readonly string[]): Promise<PapSigningLease>;
45
+ }
46
+ export interface HostSecretStore {
47
+ getEncryptionKey(namespace: string): Promise<Uint8Array | undefined>;
48
+ /** Atomic across Host processes; key is 32 random bytes and never persisted in the PAP directory. */
49
+ getOrCreateEncryptionKey(namespace: string): Promise<Uint8Array>;
50
+ }
51
+ export interface CallbackHandoff {
52
+ /** Authenticated device-bound registration; the service receives no PKCE verifier or PAP IDs. */
53
+ register(input: {
54
+ transactionId: string;
55
+ issuer: string;
56
+ clientId: string;
57
+ redirectUri: string;
58
+ state: string;
59
+ expiresAt: number;
60
+ }): Promise<{
61
+ callbackRef: string;
62
+ }>;
63
+ collect(callbackRef: string): Promise<{
64
+ state?: string;
65
+ iss?: string;
66
+ code?: string;
67
+ error?: string;
68
+ }>;
69
+ cancel?(callbackRef: string): Promise<void>;
70
+ }
71
+ export interface PapHostOptions {
72
+ binding: Binding;
73
+ identityHost: PapIdentityHost;
74
+ secretStore: HostSecretStore;
75
+ stateRoot: string;
76
+ callbackHandoff?: CallbackHandoff;
77
+ clock?: () => number;
78
+ /** Maximum local inline cooldown wait, 0..5000ms; this never shortens the persisted server fence. */
79
+ inlineCooldownWaitMs?: number;
80
+ /** Host-only observer, must not throw or log; used for private response-redaction history. */
81
+ observeCredentials?: (credentials: CredentialObservation) => void;
82
+ }
83
+ export type { AsMetadata, IdentityRef };
package/lib/host.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { PapClient } from './client.js';
2
+ export { PapError } from './errors.js';
3
+ export type { PapErrorCode, PapAuthHint } from './errors.js';
4
+ export { PapDiscovery } from './discovery.js';
5
+ export { SafeHttpsTransport } from './network.js';
6
+ export type { SafeHttpsOptions } from './network.js';
7
+ export { ProtocolIdentityHost } from './protocol-adapter.js';
8
+ export type { ProtocolIdentityClient } from './protocol-adapter.js';
9
+ export type { Binding, Company, Operation, HttpTransport, HttpResponse } from './types.js';
10
+ export type { HostSecretStore, CallbackHandoff, PapIdentityHost, PapSigningLease, PapHostOptions } from './host.js';
11
+ export { DeviceCallbackHandoff } from './callback-handoff.js';
12
+ export type { DeviceManagementOperations, CallbackManagementOperation, CallbackRegistration, CallbackClaim, CallbackRegistrationResult, CallbackClaimResult, RegisterCallbackOperation, ClaimCallbackOperation } from './callback-handoff.js';
13
+ export { PapService } from './plugin.js';
14
+ export type { PapPluginConfig, PapHostServices } from './plugin.js';
15
+ export type { PapHostConsumer, PapManagementRequest, PapOperationRequest, ProductResult } from './product-api.js';
16
+ export { FileSecretStore } from './file-secrets.js';
17
+ export { KeyringSecretStore } from './keyring.js';
18
+ /** Root Loader row declares the browser client; the capability-owning service is ./plugin. */
19
+ export declare const name = "@awiki/dsh-pap";
20
+ export declare const inject: string[];
21
+ export declare function apply(_context: import('@deepseek-ai/cordis').Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { PapClient } from './client.js';
2
+ export { PapError } from './errors.js';
3
+ export { PapDiscovery } from './discovery.js';
4
+ export { SafeHttpsTransport } from './network.js';
5
+ export { ProtocolIdentityHost } from './protocol-adapter.js';
6
+ export { DeviceCallbackHandoff } from './callback-handoff.js';
7
+ export { PapService } from './plugin.js';
8
+ export { FileSecretStore } from './file-secrets.js';
9
+ export { KeyringSecretStore } from './keyring.js';
10
+ /** Root Loader row declares the browser client; the capability-owning service is ./plugin. */
11
+ export const name = '@awiki/dsh-pap';
12
+ export const inject = [];
13
+ export function apply(_context) { }
@@ -0,0 +1,10 @@
1
+ import type { HostSecretStore } from './host.js';
2
+ /** Linux Secret Service adapter. No file fallback; a locked/missing keyring blocks credentials. */
3
+ export declare class KeyringSecretStore implements HostSecretStore {
4
+ private readonly service;
5
+ private readonly locksRoot;
6
+ constructor(service: string, locksRoot: string);
7
+ private command;
8
+ getEncryptionKey(namespace: string): Promise<Uint8Array | undefined>;
9
+ getOrCreateEncryptionKey(namespace: string): Promise<Uint8Array>;
10
+ }
package/lib/keyring.js ADDED
@@ -0,0 +1,64 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { isAbsolute } from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import lockfile from 'proper-lockfile';
6
+ import { PapError, requirePap } from './errors.js';
7
+ /** Linux Secret Service adapter. No file fallback; a locked/missing keyring blocks credentials. */
8
+ export class KeyringSecretStore {
9
+ service;
10
+ locksRoot;
11
+ constructor(service, locksRoot) {
12
+ this.service = service;
13
+ this.locksRoot = locksRoot;
14
+ requirePap(service.length > 0 && isAbsolute(locksRoot), 'credential_unavailable');
15
+ }
16
+ command(args, input) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn('secret-tool', args, { stdio: ['pipe', 'pipe', 'ignore'], env: process.env });
19
+ let value = '';
20
+ const timeout = setTimeout(() => { child.kill('SIGKILL'); reject(new PapError('credential_unavailable')); }, 5000);
21
+ child.stdout.on('data', chunk => { value += String(chunk); if (value.length > 256) {
22
+ child.kill('SIGKILL');
23
+ clearTimeout(timeout);
24
+ reject(new PapError('credential_unavailable'));
25
+ } });
26
+ child.on('error', () => { clearTimeout(timeout); reject(new PapError('credential_unavailable')); });
27
+ child.on('close', code => { clearTimeout(timeout); resolve({ code: code ?? -1, value: value.trim() }); });
28
+ child.stdin.on('error', () => { });
29
+ child.stdin.end(input === undefined ? undefined : `${input}\n`);
30
+ });
31
+ }
32
+ async getEncryptionKey(namespace) {
33
+ requirePap(process.platform === 'linux', 'credential_unavailable');
34
+ const result = await this.command(['lookup', 'service', this.service, 'namespace', namespace]);
35
+ if (result.code === 1 && result.value === '')
36
+ return undefined;
37
+ requirePap(result.code === 0 && /^[A-Za-z0-9_-]{43}$/.test(result.value), 'credential_unavailable');
38
+ return Buffer.from(result.value, 'base64url');
39
+ }
40
+ async getOrCreateEncryptionKey(namespace) {
41
+ requirePap(process.platform === 'linux', 'credential_unavailable');
42
+ await mkdir(this.locksRoot, { recursive: true, mode: 0o700 });
43
+ const release = await lockfile.lock(this.locksRoot, { retries: { retries: 60, minTimeout: 50, maxTimeout: 100 }, stale: 30000 });
44
+ try {
45
+ const attributes = ['service', this.service, 'namespace', namespace];
46
+ const existing = await this.command(['lookup', ...attributes]);
47
+ if (existing.code === 0) {
48
+ requirePap(/^[A-Za-z0-9_-]{43}$/.test(existing.value), 'credential_unavailable');
49
+ return Buffer.from(existing.value, 'base64url');
50
+ }
51
+ requirePap(existing.code === 1 && existing.value === '', 'credential_unavailable');
52
+ const key = randomBytes(32);
53
+ const encoded = key.toString('base64url');
54
+ const stored = await this.command(['store', '--label=DSH PAP encrypted state', ...attributes], encoded);
55
+ requirePap(stored.code === 0, 'credential_unavailable');
56
+ const checked = await this.command(['lookup', ...attributes]);
57
+ requirePap(checked.code === 0 && checked.value === encoded, 'credential_unavailable');
58
+ return key;
59
+ }
60
+ finally {
61
+ await release();
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,13 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
3
+ import type { PapManagementRequest } from './product-api.js';
4
+ /** Mounted only on the authenticated user management Gateway, never an Agent tool. */
5
+ export default class PapManagerRemote extends TypertRemoteService {
6
+ static inject: string[];
7
+ private readonly manager;
8
+ constructor(ctx: Context);
9
+ status(): Promise<import("./product-api.js").ProductResult>;
10
+ operations(): Promise<import("./product-api.js").ProductResult>;
11
+ /** Browser authorization URL is returned only through this user management endpoint. */
12
+ manage(input: PapManagementRequest): Promise<import("./product-api.js").ProductResult>;
13
+ }
@@ -0,0 +1,69 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
36
+ import contribution from './remote.js';
37
+ let PapManagerRemote = (() => {
38
+ let _classSuper = TypertRemoteService;
39
+ let _instanceExtraInitializers = [];
40
+ let _status_decorators;
41
+ let _operations_decorators;
42
+ let _manage_decorators;
43
+ return class PapManagerRemote extends _classSuper {
44
+ static {
45
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
46
+ _status_decorators = [Remote];
47
+ _operations_decorators = [Remote];
48
+ _manage_decorators = [Remote];
49
+ __esDecorate(this, null, _status_decorators, { kind: "method", name: "status", static: false, private: false, access: { has: obj => "status" in obj, get: obj => obj.status }, metadata: _metadata }, null, _instanceExtraInitializers);
50
+ __esDecorate(this, null, _operations_decorators, { kind: "method", name: "operations", static: false, private: false, access: { has: obj => "operations" in obj, get: obj => obj.operations }, metadata: _metadata }, null, _instanceExtraInitializers);
51
+ __esDecorate(this, null, _manage_decorators, { kind: "method", name: "manage", static: false, private: false, access: { has: obj => "manage" in obj, get: obj => obj.manage }, metadata: _metadata }, null, _instanceExtraInitializers);
52
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
53
+ }
54
+ static inject = ['pap', 'typert'];
55
+ manager = __runInitializers(this, _instanceExtraInitializers);
56
+ constructor(ctx) {
57
+ super(ctx, 'papManager');
58
+ this.manager = ctx.pap.acquireManagement(ctx);
59
+ ctx.effect(() => ctx.typert.register({ package: contribution.package, face: 'host', schemas: [],
60
+ model: { services: [], events: [], objects: [] }, invocations: contribution.descriptors }));
61
+ }
62
+ status() { return this.manager.status(); }
63
+ operations() { return this.manager.operations(); }
64
+ /** Browser authorization URL is returned only through this user management endpoint. */
65
+ manage(input) { return this.manager.execute(input); }
66
+ };
67
+ })();
68
+ /** Mounted only on the authenticated user management Gateway, never an Agent tool. */
69
+ export default PapManagerRemote;
@@ -0,0 +1,23 @@
1
+ import type { HttpResponse, HttpTransport } from './types.js';
2
+ export declare function isPublicAddress(address: string): boolean;
3
+ export interface SafeHttpsOptions {
4
+ timeoutMs?: number;
5
+ maxBytes?: number;
6
+ maxRequestBytes?: number;
7
+ maxConcurrent?: number;
8
+ /** Explicit isolated deployment/test exception; exact origins only. TLS still verified. */
9
+ privateOrigins?: readonly string[];
10
+ ca?: string | Buffer;
11
+ /** Exact-origin mapping for isolated HTTPS component environments; requires privateOrigins. */
12
+ portOverrides?: Readonly<Record<string, number>>;
13
+ lookup?: (host: string) => Promise<{
14
+ address: string;
15
+ family: number;
16
+ }[]>;
17
+ }
18
+ export declare class SafeHttpsTransport implements HttpTransport {
19
+ private readonly options;
20
+ private active;
21
+ constructor(options?: SafeHttpsOptions);
22
+ request(value: string, options?: Parameters<HttpTransport['request']>[1]): Promise<HttpResponse>;
23
+ }
package/lib/network.js ADDED
@@ -0,0 +1,91 @@
1
+ import https from 'node:https';
2
+ import { lookup as dnsLookup } from 'node:dns/promises';
3
+ import { BlockList, isIP } from 'node:net';
4
+ import { PapError, requirePap } from './errors.js';
5
+ import { httpsUrl } from './util.js';
6
+ const denied = new BlockList();
7
+ for (const [address, prefix] of [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]])
8
+ denied.addSubnet(address, prefix, 'ipv4');
9
+ const deniedV6 = new BlockList();
10
+ for (const [address, prefix] of [['2001::', 23], ['2001:db8::', 32], ['2002::', 16], ['3fff::', 20]])
11
+ deniedV6.addSubnet(address, prefix, 'ipv6');
12
+ export function isPublicAddress(address) {
13
+ if (isIP(address) === 4)
14
+ return !denied.check(address, 'ipv4');
15
+ if (isIP(address) !== 6)
16
+ return false;
17
+ // Only globally routable unicast IPv6; mapped IPv4 and special ranges are excluded.
18
+ const first = Number.parseInt(address.split(':')[0], 16);
19
+ return first >= 0x2000 && first <= 0x3fff && !deniedV6.check(address, 'ipv6');
20
+ }
21
+ export class SafeHttpsTransport {
22
+ options;
23
+ active = 0;
24
+ constructor(options = {}) {
25
+ this.options = options;
26
+ }
27
+ async request(value, options = {}) {
28
+ const url = httpsUrl(value);
29
+ requirePap(url.href.length <= 8192 && Buffer.byteLength(options.body ?? '') <= (this.options.maxRequestBytes ?? 1024 * 1024), 'unsafe_destination');
30
+ requirePap(Object.keys(options.headers ?? {}).length <= 64 && Object.entries(options.headers ?? {}).every(([key, value]) => key.length + value.length <= 131072 && !/[\r\n]/.test(key + value)), 'unsafe_destination');
31
+ requirePap(this.active < (this.options.maxConcurrent ?? 8), 'rate_limited');
32
+ this.active++;
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 10000);
35
+ const abort = () => controller.abort();
36
+ options.signal?.addEventListener('abort', abort, { once: true });
37
+ try {
38
+ const host = url.hostname.replace(/^\[|\]$/g, '');
39
+ const lookup = this.options.lookup ?? (async (name) => dnsLookup(name, { all: true, verbatim: true }));
40
+ const addresses = await Promise.race([
41
+ isIP(host) ? Promise.resolve([{ address: host, family: isIP(host) }]) : lookup(host),
42
+ new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new PapError('request_uncertain')), { once: true })),
43
+ ]);
44
+ requirePap(addresses.length > 0 && addresses.length <= 32, 'unsafe_destination');
45
+ const allowPrivate = this.options.privateOrigins?.includes(url.origin) === true;
46
+ requirePap(allowPrivate || addresses.every(item => isPublicAddress(item.address)), 'unsafe_destination');
47
+ requirePap(!controller.signal.aborted && !options.signal?.aborted, 'request_uncertain');
48
+ // Resolution is pinned to this connection. The HTTP stack never resolves the host again.
49
+ const address = addresses[0];
50
+ return await new Promise((resolve, reject) => {
51
+ const request = https.request(url, {
52
+ method: options.method ?? 'GET', headers: { ...options.headers, host: url.host }, signal: controller.signal,
53
+ ca: this.options.ca, agent: false,
54
+ ...(this.options.portOverrides?.[url.origin] !== undefined && allowPrivate ? { port: this.options.portOverrides[url.origin] } : {}),
55
+ lookup: ((_hostname, _options, callback) => {
56
+ if (_options.all)
57
+ callback(null, [address]);
58
+ else
59
+ callback(null, address.address, address.family);
60
+ }),
61
+ }, response => {
62
+ const chunks = [];
63
+ let bytes = 0;
64
+ response.on('data', (chunk) => {
65
+ bytes += chunk.length;
66
+ if (bytes > (this.options.maxBytes ?? 1024 * 1024)) {
67
+ response.destroy();
68
+ reject(new PapError('request_uncertain'));
69
+ }
70
+ else
71
+ chunks.push(chunk);
72
+ });
73
+ response.on('error', () => reject(new PapError('request_uncertain')));
74
+ response.on('end', () => resolve({
75
+ status: response.statusCode ?? 0, body: Buffer.concat(chunks), url: url.href,
76
+ headers: Object.fromEntries(Object.entries(response.headers).map(([key, value]) => [key, Array.isArray(value) ? value.join(', ') : value ?? ''])),
77
+ }));
78
+ });
79
+ request.on('error', () => reject(new PapError('request_uncertain')));
80
+ if (options.body !== undefined)
81
+ request.write(options.body);
82
+ request.end();
83
+ });
84
+ }
85
+ finally {
86
+ clearTimeout(timeout);
87
+ options.signal?.removeEventListener('abort', abort);
88
+ this.active--;
89
+ }
90
+ }
91
+ }