@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.
- package/LICENSE +202 -0
- package/README.md +197 -2
- package/cordis.patch.yml +15 -0
- package/docs/verification.md +217 -0
- package/lib/auth-challenge.d.ts +19 -0
- package/lib/auth-challenge.js +136 -0
- package/lib/browser-client.js +65 -0
- package/lib/browser-client.js.map +6 -0
- package/lib/callback-handoff.d.ts +55 -0
- package/lib/callback-handoff.js +33 -0
- package/lib/client/controller.d.ts +107 -0
- package/lib/client/controller.js +106 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/index.js +28 -0
- package/lib/client/views.d.ts +5 -0
- package/lib/client/views.js +28 -0
- package/lib/client.d.ts +108 -0
- package/lib/client.js +833 -0
- package/lib/cooldown.d.ts +11 -0
- package/lib/cooldown.js +88 -0
- package/lib/credential-history.d.ts +33 -0
- package/lib/credential-history.js +109 -0
- package/lib/delegation.d.ts +16 -0
- package/lib/delegation.js +59 -0
- package/lib/discovery.d.ts +8 -0
- package/lib/discovery.js +227 -0
- package/lib/errors.d.ts +14 -0
- package/lib/errors.js +17 -0
- package/lib/file-secrets.d.ts +14 -0
- package/lib/file-secrets.js +148 -0
- package/lib/host-secrets.d.ts +16 -0
- package/lib/host-secrets.js +22 -0
- package/lib/host.d.ts +83 -0
- package/lib/host.js +1 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +13 -0
- package/lib/keyring.d.ts +10 -0
- package/lib/keyring.js +64 -0
- package/lib/management-remote.d.ts +13 -0
- package/lib/management-remote.js +69 -0
- package/lib/network.d.ts +23 -0
- package/lib/network.js +91 -0
- package/lib/plugin.d.ts +75 -0
- package/lib/plugin.js +473 -0
- package/lib/product-api.d.ts +89 -0
- package/lib/product-api.js +25 -0
- package/lib/product-operations.d.ts +16 -0
- package/lib/product-operations.js +120 -0
- package/lib/protocol-adapter.d.ts +90 -0
- package/lib/protocol-adapter.js +75 -0
- package/lib/remote.d.ts +3 -0
- package/lib/remote.js +17 -0
- package/lib/store.d.ts +14 -0
- package/lib/store.js +128 -0
- package/lib/strict-json.d.ts +2 -0
- package/lib/strict-json.js +111 -0
- package/lib/tools.d.ts +5 -0
- package/lib/tools.js +29 -0
- package/lib/types.d.ts +202 -0
- package/lib/types.js +1 -0
- package/lib/util.d.ts +17 -0
- package/lib/util.js +66 -0
- package/package.json +155 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CallbackHandoff } from './host.js';
|
|
2
|
+
export interface CallbackRegistration {
|
|
3
|
+
transaction_id: string;
|
|
4
|
+
client_id: string;
|
|
5
|
+
issuer: string;
|
|
6
|
+
redirect_uri: string;
|
|
7
|
+
state_sha256: string;
|
|
8
|
+
claim_sha256: string;
|
|
9
|
+
expires_at: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CallbackClaim {
|
|
12
|
+
transaction_id: string;
|
|
13
|
+
claim_secret: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CallbackRegistrationResult {
|
|
16
|
+
transaction_id: string;
|
|
17
|
+
callback_uri: string;
|
|
18
|
+
expires_at: string;
|
|
19
|
+
}
|
|
20
|
+
export type CallbackClaimResult = {
|
|
21
|
+
state: string;
|
|
22
|
+
iss: string;
|
|
23
|
+
code: string;
|
|
24
|
+
error?: never;
|
|
25
|
+
} | {
|
|
26
|
+
state: string;
|
|
27
|
+
iss: string;
|
|
28
|
+
error: string;
|
|
29
|
+
code?: never;
|
|
30
|
+
};
|
|
31
|
+
export interface RegisterCallbackOperation {
|
|
32
|
+
action: 'pap-callback-register';
|
|
33
|
+
operationId: string;
|
|
34
|
+
callback: CallbackRegistration;
|
|
35
|
+
}
|
|
36
|
+
export interface ClaimCallbackOperation {
|
|
37
|
+
action: 'pap-callback-claim';
|
|
38
|
+
operationId: string;
|
|
39
|
+
callback: CallbackClaim;
|
|
40
|
+
}
|
|
41
|
+
export type CallbackManagementOperation = RegisterCallbackOperation | ClaimCallbackOperation;
|
|
42
|
+
export interface DeviceManagementOperations {
|
|
43
|
+
/** The authenticated C Host driver supplies checkpoint/device proof and only the two fixed B routes. */
|
|
44
|
+
execute(input: RegisterCallbackOperation): Promise<CallbackRegistrationResult>;
|
|
45
|
+
execute(input: ClaimCallbackOperation): Promise<CallbackClaimResult>;
|
|
46
|
+
}
|
|
47
|
+
/** Fixed B callback relay, with an independent pickup secret. callbackRef is encrypted by PapStore. */
|
|
48
|
+
export declare class DeviceCallbackHandoff implements CallbackHandoff {
|
|
49
|
+
private readonly management;
|
|
50
|
+
constructor(management: DeviceManagementOperations);
|
|
51
|
+
register(input: Parameters<CallbackHandoff['register']>[0]): Promise<{
|
|
52
|
+
callbackRef: string;
|
|
53
|
+
}>;
|
|
54
|
+
collect(callbackRef: string): ReturnType<CallbackHandoff['collect']>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { PapError, requirePap } from './errors.js';
|
|
3
|
+
const hex = (value) => createHash('sha256').update(value, 'utf8').digest('hex');
|
|
4
|
+
/** Fixed B callback relay, with an independent pickup secret. callbackRef is encrypted by PapStore. */
|
|
5
|
+
export class DeviceCallbackHandoff {
|
|
6
|
+
management;
|
|
7
|
+
constructor(management) {
|
|
8
|
+
this.management = management;
|
|
9
|
+
}
|
|
10
|
+
async register(input) {
|
|
11
|
+
const claimSecret = randomBytes(32).toString('base64url');
|
|
12
|
+
const registration = {
|
|
13
|
+
transaction_id: input.transactionId, client_id: input.clientId, issuer: input.issuer, redirect_uri: input.redirectUri,
|
|
14
|
+
state_sha256: hex(input.state), claim_sha256: hex(claimSecret), expires_at: new Date(input.expiresAt).toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
15
|
+
};
|
|
16
|
+
const response = await this.management.execute({ action: 'pap-callback-register', operationId: `register_${input.transactionId}`, callback: registration });
|
|
17
|
+
requirePap(response.transaction_id === input.transactionId && response.callback_uri === input.redirectUri, 'invalid_authorization_response');
|
|
18
|
+
return { callbackRef: JSON.stringify({ transactionId: input.transactionId, claimSecret, expiresAt: input.expiresAt }) };
|
|
19
|
+
}
|
|
20
|
+
async collect(callbackRef) {
|
|
21
|
+
let reference;
|
|
22
|
+
try {
|
|
23
|
+
reference = JSON.parse(callbackRef);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new PapError('invalid_authorization_response');
|
|
27
|
+
}
|
|
28
|
+
requirePap(typeof reference.transactionId === 'string' && typeof reference.claimSecret === 'string' && /^[A-Za-z0-9_-]{43}$/.test(reference.claimSecret) && reference.expiresAt > Date.now(), 'invalid_authorization_response');
|
|
29
|
+
const response = await this.management.execute({ action: 'pap-callback-claim', operationId: `claim_${reference.transactionId}`, callback: { transaction_id: reference.transactionId, claim_secret: reference.claimSecret } });
|
|
30
|
+
requirePap(typeof response.state === 'string' && typeof response.iss === 'string' && ((typeof response.code === 'string' && !response.error) || (typeof response.error === 'string' && !response.code)), 'invalid_authorization_response');
|
|
31
|
+
return { state: response.state, iss: response.iss, code: response.code, error: response.error };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PapManagementRequest } from '../product-api.js';
|
|
3
|
+
declare const reference: z.ZodObject<{
|
|
4
|
+
storeId: z.ZodString;
|
|
5
|
+
identityId: z.ZodString;
|
|
6
|
+
did: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
declare const status: z.ZodObject<{
|
|
9
|
+
state: z.ZodString;
|
|
10
|
+
error: z.ZodOptional<z.ZodString>;
|
|
11
|
+
identity: z.ZodOptional<z.ZodObject<{
|
|
12
|
+
storeId: z.ZodString;
|
|
13
|
+
identityId: z.ZodString;
|
|
14
|
+
did: z.ZodString;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
companies: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
17
|
+
companyRef: z.ZodString;
|
|
18
|
+
name: z.ZodString;
|
|
19
|
+
authMode: z.ZodEnum<{
|
|
20
|
+
no_as: "no_as";
|
|
21
|
+
account: "account";
|
|
22
|
+
disabled: "disabled";
|
|
23
|
+
}>;
|
|
24
|
+
scopes: z.ZodArray<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>>>;
|
|
26
|
+
sessions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
27
|
+
sessionRef: z.ZodString;
|
|
28
|
+
companyRef: z.ZodOptional<z.ZodString>;
|
|
29
|
+
connected: z.ZodBoolean;
|
|
30
|
+
account: z.ZodBoolean;
|
|
31
|
+
accountSlot: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>>>;
|
|
33
|
+
accounts: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
34
|
+
accountSlot: z.ZodString;
|
|
35
|
+
companyRef: z.ZodOptional<z.ZodString>;
|
|
36
|
+
status: z.ZodString;
|
|
37
|
+
scopes: z.ZodArray<z.ZodString>;
|
|
38
|
+
canRefresh: z.ZodDefault<z.ZodBoolean>;
|
|
39
|
+
}, z.core.$strip>>>;
|
|
40
|
+
pendingSignIns: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
41
|
+
transactionRef: z.ZodString;
|
|
42
|
+
accountSlot: z.ZodString;
|
|
43
|
+
companyRef: z.ZodOptional<z.ZodString>;
|
|
44
|
+
expiresAt: z.ZodNumber;
|
|
45
|
+
}, z.core.$strip>>>;
|
|
46
|
+
authenticationHints: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
47
|
+
operationRef: z.ZodString;
|
|
48
|
+
scheme: z.ZodEnum<{
|
|
49
|
+
Bearer: "Bearer";
|
|
50
|
+
DPoP: "DPoP";
|
|
51
|
+
}>;
|
|
52
|
+
scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
53
|
+
resourceMetadataUrl: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$strip>>>;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
declare const operation: z.ZodObject<{
|
|
57
|
+
operationRef: z.ZodString;
|
|
58
|
+
companyRef: z.ZodString;
|
|
59
|
+
method: z.ZodString;
|
|
60
|
+
path: z.ZodString;
|
|
61
|
+
enabled: z.ZodBoolean;
|
|
62
|
+
account: z.ZodBoolean;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
export interface UserPapRemote {
|
|
65
|
+
status(): Promise<any>;
|
|
66
|
+
operations(): Promise<any>;
|
|
67
|
+
manage(input: PapManagementRequest): Promise<any>;
|
|
68
|
+
}
|
|
69
|
+
export interface PublicIdentityRemote {
|
|
70
|
+
listIdentities(): Promise<any>;
|
|
71
|
+
publicationStatus(identity: z.infer<typeof reference>): Promise<any>;
|
|
72
|
+
}
|
|
73
|
+
export interface SettingsView {
|
|
74
|
+
status: z.infer<typeof status>;
|
|
75
|
+
operations: z.infer<typeof operation>[];
|
|
76
|
+
identities: {
|
|
77
|
+
reference: z.infer<typeof reference>;
|
|
78
|
+
label: string;
|
|
79
|
+
}[];
|
|
80
|
+
loading: boolean;
|
|
81
|
+
pending: boolean;
|
|
82
|
+
waitingIdentity: boolean;
|
|
83
|
+
error: string | null;
|
|
84
|
+
authorization: {
|
|
85
|
+
transactionRef: string;
|
|
86
|
+
url: string;
|
|
87
|
+
} | null;
|
|
88
|
+
}
|
|
89
|
+
export declare const identityKey: (value: z.infer<typeof reference>) => string;
|
|
90
|
+
export declare class PapSettingsController {
|
|
91
|
+
private readonly remotes;
|
|
92
|
+
private view;
|
|
93
|
+
private readonly listeners;
|
|
94
|
+
private disposed;
|
|
95
|
+
private refreshing;
|
|
96
|
+
constructor(remotes: () => {
|
|
97
|
+
pap?: UserPapRemote;
|
|
98
|
+
identities?: PublicIdentityRemote;
|
|
99
|
+
});
|
|
100
|
+
getSnapshot: () => SettingsView;
|
|
101
|
+
subscribe: (listener: () => void) => () => void;
|
|
102
|
+
private patch;
|
|
103
|
+
dispose(): void;
|
|
104
|
+
refresh(): Promise<void>;
|
|
105
|
+
execute(input: PapManagementRequest): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const reference = z.object({ storeId: z.string(), identityId: z.string(), did: z.string() });
|
|
3
|
+
const company = z.object({ companyRef: z.string(), name: z.string(), authMode: z.enum(['no_as', 'account', 'disabled']), scopes: z.array(z.string()) });
|
|
4
|
+
const session = z.object({ sessionRef: z.string(), companyRef: z.string().optional(), connected: z.boolean(), account: z.boolean(), accountSlot: z.string().optional() });
|
|
5
|
+
const account = z.object({ accountSlot: z.string(), companyRef: z.string().optional(), status: z.string(), scopes: z.array(z.string()), canRefresh: z.boolean().default(false) });
|
|
6
|
+
const pending = z.object({ transactionRef: z.string(), accountSlot: z.string(), companyRef: z.string().optional(), expiresAt: z.number() });
|
|
7
|
+
const hint = z.object({ operationRef: z.string(), scheme: z.enum(['Bearer', 'DPoP']), scope: z.array(z.string()).optional(), resourceMetadataUrl: z.string().optional() });
|
|
8
|
+
const status = z.object({ state: z.string(), error: z.string().optional(), identity: reference.optional(), companies: z.array(company).default([]), sessions: z.array(session).default([]), accounts: z.array(account).default([]), pendingSignIns: z.array(pending).default([]), authenticationHints: z.array(hint).default([]) });
|
|
9
|
+
const operation = z.object({ operationRef: z.string(), companyRef: z.string(), method: z.string(), path: z.string(), enabled: z.boolean(), account: z.boolean() });
|
|
10
|
+
const messages = {
|
|
11
|
+
credential_history_incomplete: '旧账户凭据历史不完整。请先处理原账户并由受信管理核验,不能继续提供给 Agent。',
|
|
12
|
+
not_configured: '请先在 Host 配置中启用 PAP,并指定已批准的身份政策。', credential_unavailable: '受保护凭据暂不可用,请检查系统秘密存储后重试。',
|
|
13
|
+
permission_denied: '当前操作未获允许,请检查身份、连接与权限。', consumer_forbidden: 'PAP 身份许可暂不可用。', provider_disposed: '身份服务已更新,请重新选择身份。',
|
|
14
|
+
not_published: '请先在身份设置中完成注册和发布。', unsupported_sign_in: '此 Company 当前不支持发起账户登录。', insufficient_scope: '请选择此 Company 提供的账户权限。',
|
|
15
|
+
callback_not_ready: '浏览器登录尚未完成,请完成后再次领取。', callback_claimed: '该登录结果已领取,请刷新状态或重新发起。', callback_expired: '登录已过期,请重新发起。',
|
|
16
|
+
reauthorization_required: '账户需要重新登录。', revocation_pending: '本地已停止账户使用,远端撤销仍待确认。', state_conflict: '连接状态已变化,请刷新后重试。',
|
|
17
|
+
disconnected: '连接已停止,请刷新状态。', request_uncertain: '请求结果不确定,请先确认远端状态。', rate_limited: '请求暂受限,请稍后重试。',
|
|
18
|
+
};
|
|
19
|
+
class SafeMessage extends Error {
|
|
20
|
+
displayText;
|
|
21
|
+
constructor(displayText) {
|
|
22
|
+
super('PAP user operation unavailable');
|
|
23
|
+
this.displayText = displayText;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function unwrap(response) {
|
|
27
|
+
if (!response || response.ok !== true || !response.value)
|
|
28
|
+
throw new SafeMessage('等待 PAP Host 服务,请连接后刷新。');
|
|
29
|
+
if (response.value.ok !== true)
|
|
30
|
+
throw new SafeMessage(messages[response.value.error?.code] ?? '操作暂不可用,请刷新状态后重试。');
|
|
31
|
+
return response.value.value;
|
|
32
|
+
}
|
|
33
|
+
export const identityKey = (value) => JSON.stringify([value.storeId, value.identityId, value.did]);
|
|
34
|
+
export class PapSettingsController {
|
|
35
|
+
remotes;
|
|
36
|
+
view = { status: { state: 'waiting', companies: [], sessions: [], accounts: [], pendingSignIns: [], authenticationHints: [] }, operations: [], identities: [], loading: true, pending: false, waitingIdentity: true, error: null, authorization: null };
|
|
37
|
+
listeners = new Set();
|
|
38
|
+
disposed = false;
|
|
39
|
+
refreshing = false;
|
|
40
|
+
constructor(remotes) {
|
|
41
|
+
this.remotes = remotes;
|
|
42
|
+
}
|
|
43
|
+
getSnapshot = () => this.view;
|
|
44
|
+
subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
|
|
45
|
+
patch(value) { if (this.disposed)
|
|
46
|
+
return; this.view = { ...this.view, ...value }; for (const listener of this.listeners)
|
|
47
|
+
listener(); }
|
|
48
|
+
dispose() { this.disposed = true; this.listeners.clear(); this.view.authorization = null; }
|
|
49
|
+
async refresh() {
|
|
50
|
+
if (this.refreshing || this.disposed)
|
|
51
|
+
return;
|
|
52
|
+
this.refreshing = true;
|
|
53
|
+
try {
|
|
54
|
+
const remote = this.remotes();
|
|
55
|
+
if (!remote.pap)
|
|
56
|
+
throw new SafeMessage('等待 PAP Host 服务,请连接后刷新。');
|
|
57
|
+
const snapshot = status.parse(unwrap(await remote.pap.status()));
|
|
58
|
+
const operations = snapshot.state === 'ready' ? z.array(operation).parse(unwrap(await remote.pap.operations())) : [];
|
|
59
|
+
let identities = [];
|
|
60
|
+
let waitingIdentity = true;
|
|
61
|
+
if (remote.identities)
|
|
62
|
+
try {
|
|
63
|
+
const result = await remote.identities.listIdentities();
|
|
64
|
+
if (result?.ok && Array.isArray(result.value)) {
|
|
65
|
+
const candidates = result.value.flatMap((item) => { const parsed = z.object({ reference, label: z.string().optional(), handle: z.string().optional(), catalogState: z.string() }).safeParse(item); return parsed.success && parsed.data.catalogState === 'active' ? [{ reference: parsed.data.reference, label: parsed.data.label ?? parsed.data.handle ?? parsed.data.reference.did }] : []; });
|
|
66
|
+
identities = (await Promise.all(candidates.map(async (item) => { const publication = await remote.identities.publicationStatus(item.reference); return publication?.ok && publication.value?.state === 'published' && publication.value.capabilities?.papSession === true ? item : undefined; }))).filter((item) => Boolean(item));
|
|
67
|
+
waitingIdentity = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
this.patch({ status: snapshot, operations, identities, waitingIdentity, loading: false, error: null,
|
|
72
|
+
...(this.view.authorization && !snapshot.pendingSignIns.some(item => item.transactionRef === this.view.authorization.transactionRef) ? { authorization: null } : {}) });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.patch({ loading: false, authorization: null, error: error instanceof SafeMessage ? error.displayText : 'PAP 状态暂不可用,请刷新后重试。' });
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.refreshing = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async execute(input) {
|
|
82
|
+
if (this.view.pending || this.disposed)
|
|
83
|
+
return;
|
|
84
|
+
this.patch({ pending: true, error: null, ...(input.action === 'identity/select' ? { authorization: null } : {}) });
|
|
85
|
+
try {
|
|
86
|
+
const remote = this.remotes().pap;
|
|
87
|
+
if (!remote)
|
|
88
|
+
throw new SafeMessage('等待 PAP Host 服务,请连接后刷新。');
|
|
89
|
+
const value = unwrap(await remote.manage(input));
|
|
90
|
+
if (input.action === 'account/sign-in') {
|
|
91
|
+
const result = z.object({ transactionRef: z.string(), authorizationUrl: z.string() }).parse(value);
|
|
92
|
+
const url = new URL(result.authorizationUrl);
|
|
93
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash)
|
|
94
|
+
throw new SafeMessage('登录链接暂不可用,请重新发起。');
|
|
95
|
+
this.patch({ authorization: { transactionRef: result.transactionRef, url: url.href } });
|
|
96
|
+
}
|
|
97
|
+
await this.refresh();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.patch({ error: error instanceof SafeMessage ? error.displayText : 'PAP 操作暂不可用,请刷新后重试。' });
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.patch({ pending: false });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { PapSettingsController } from './controller.js';
|
|
3
|
+
import { PapSettings } from './views.js';
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export declare function apply(ctx: Context): Promise<() => Promise<void>>;
|
|
6
|
+
export { PapSettingsController, PapSettings };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import contribution from '../remote.js';
|
|
2
|
+
import { PapSettingsController } from './controller.js';
|
|
3
|
+
import { PapSettings } from './views.js';
|
|
4
|
+
export const inject = ['slots', 'remote', 'connection'];
|
|
5
|
+
export async function apply(ctx) {
|
|
6
|
+
let unmount;
|
|
7
|
+
try {
|
|
8
|
+
unmount = await ctx.remote.$mount(contribution);
|
|
9
|
+
}
|
|
10
|
+
catch { }
|
|
11
|
+
const controller = new PapSettingsController(() => {
|
|
12
|
+
const identities = ctx.get('remote.anpIdentityManager');
|
|
13
|
+
return { pap: ctx.get('remote.papManager'),
|
|
14
|
+
identities: identities ? { listIdentities: () => identities.listIdentities(), publicationStatus: reference => identities.publicationStatus(reference) } : undefined };
|
|
15
|
+
});
|
|
16
|
+
let unregister;
|
|
17
|
+
try {
|
|
18
|
+
unregister = ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'pap-companies', label: 'PAP 连接', order: 45, inject: () => ({ controller }) }, PapSettings));
|
|
19
|
+
void controller.refresh();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
controller.dispose();
|
|
23
|
+
await unmount?.();
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return async () => { controller.dispose(); unregister(); await unmount?.(); };
|
|
27
|
+
}
|
|
28
|
+
export { PapSettingsController, PapSettings };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { PapSettingsController, identityKey } from './controller.js';
|
|
4
|
+
const stateNames = { disabled: '未启用', unconfigured: '未配置身份', credential_unavailable: '受保护凭据不可用', migration_required: '旧凭据历史需要管理处理', ready: '可用', waiting: '等待 Host' };
|
|
5
|
+
function httpsLink(value) { try {
|
|
6
|
+
const url = new URL(value ?? '');
|
|
7
|
+
return url.protocol === 'https:' && !url.username && !url.password && !url.hash ? url.href : undefined;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
} }
|
|
12
|
+
export function PapSettings({ controller }) {
|
|
13
|
+
const view = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
14
|
+
const [identity, setIdentity] = useState('');
|
|
15
|
+
const [domain, setDomain] = useState('');
|
|
16
|
+
const busy = view.pending || view.loading;
|
|
17
|
+
const run = (input) => { void controller.execute(input); };
|
|
18
|
+
return _jsxs("section", { "aria-label": "PAP \u8BBE\u7F6E", style: { display: 'grid', gap: 12, maxWidth: 960 }, children: [_jsx("h2", { children: "PAP Company \u8FDE\u63A5" }), _jsxs("p", { role: "status", children: ["\u72B6\u6001\uFF1A", stateNames[view.status.state] ?? '暂不可用'] }), _jsx("p", { children: "\u5148\u9009\u62E9\u5DF2\u53D1\u5E03\u8EAB\u4EFD\uFF0C\u518D\u53D1\u73B0 Company \u5E76\u5EFA\u7ACB\u8FDE\u63A5\u3002\u53EA\u6709\u4F60\u660E\u786E\u5141\u8BB8\u7684\u64CD\u4F5C\u4F1A\u63D0\u4F9B\u7ED9 Agent\u3002" }), _jsx("button", { disabled: busy, onClick: () => { void controller.refresh(); }, children: "\u5237\u65B0\u72B6\u6001" }), view.error && _jsx("p", { role: "alert", children: view.error }), view.status.authenticationHints.map(hint => _jsxs("p", { role: "status", children: ["\u8BE5\u64CD\u4F5C\u9700\u8981\u68C0\u67E5\u8D26\u6237\u6743\u9650\uFF1A", hint.scope?.join('、') ?? '请检查当前账户授权', " ", httpsLink(hint.resourceMetadataUrl) && _jsx("a", { href: httpsLink(hint.resourceMetadataUrl), target: "_blank", rel: "noopener noreferrer", children: "\u67E5\u770B\u8D44\u6E90\u6388\u6743\u8BF4\u660E" })] }, hint.operationRef)), view.status.state === 'disabled' && _jsx("p", { children: "\u8BF7\u5148\u5728 Host \u914D\u7F6E\u4E2D\u542F\u7528 PAP\uFF0C\u5E76\u914D\u7F6E\u5DF2\u6279\u51C6\u7684\u8EAB\u4EFD\u653F\u7B56\u3002" }), view.status.state === 'migration_required' && _jsxs("div", { children: [_jsx("p", { children: "\u65E7\u8D26\u6237\u51ED\u636E\u5386\u53F2\u4E0D\u5B8C\u6574\uFF0CAgent \u64CD\u4F5C\u5DF2\u505C\u6B62\u3002\u9000\u51FA\u5F53\u524D\u8D26\u6237\u4E0D\u4F1A\u8BC1\u660E\u5168\u90E8\u65E7\u51ED\u636E\u5DF2\u5931\u6548\uFF1B\u8BF7\u4FDD\u7559\u52A0\u5BC6\u72B6\u6001\uFF0C\u7531\u53D7\u4FE1\u7BA1\u7406\u6838\u9A8C\u5168\u90E8\u65E7\u51ED\u636E\u64A4\u9500\u540E\u5904\u7406\u3002" }), view.status.accounts.map(account => _jsxs("p", { children: ["\u8D26\u6237 ", account.accountSlot.slice(-8), " \u00B7 ", account.status, " ", _jsx("button", { disabled: busy || account.status === 'signed_out', onClick: () => run({ action: 'account/sign-out', accountSlot: account.accountSlot }), children: "\u64A4\u9500\u5F53\u524D\u8D26\u6237" })] }, account.accountSlot))] }), view.waitingIdentity && _jsx("p", { role: "status", children: "\u7B49\u5F85\u8EAB\u4EFD\u7BA1\u7406\u670D\u52A1\u3002\u8BF7\u5148\u5728\u201C\u8EAB\u4EFD\u201D\u8BBE\u7F6E\u4E2D\u5B8C\u6210\u6CE8\u518C\u548C\u53D1\u5E03\uFF0C\u518D\u5237\u65B0\u3002" }), !view.waitingIdentity && view.identities.length === 0 && _jsx("p", { children: "\u6682\u65E0\u652F\u6301 PAP Session \u7684\u5DF2\u53D1\u5E03\u8EAB\u4EFD\uFF0C\u8BF7\u5148\u5728\u201C\u8EAB\u4EFD\u201D\u8BBE\u7F6E\u4E2D\u5B8C\u6210\u53D1\u5E03\u3002" }), _jsxs("form", { onSubmit: event => { event.preventDefault(); const selected = view.identities.find(item => identityKey(item.reference) === identity); if (selected)
|
|
19
|
+
run({ action: 'identity/select', reference: selected.reference }); }, children: [_jsxs("label", { children: ["\u5DF2\u53D1\u5E03\u8EAB\u4EFD ", _jsxs("select", { "aria-label": "PAP \u8EAB\u4EFD", value: identity, disabled: busy || view.status.state === 'disabled', onChange: event => setIdentity(event.target.value), children: [_jsx("option", { value: "", children: "\u8BF7\u9009\u62E9\u8EAB\u4EFD" }), view.identities.map(item => _jsx("option", { value: identityKey(item.reference), children: item.label }, identityKey(item.reference)))] })] }), _jsx("button", { disabled: busy || !identity || view.status.state === 'disabled', children: "\u4F7F\u7528\u6B64\u8EAB\u4EFD" })] }), _jsxs("form", { onSubmit: event => { event.preventDefault(); run({ action: 'company/discover', domain: domain.trim() }); }, children: [_jsxs("label", { children: ["Company \u57DF\u540D ", _jsx("input", { "aria-label": "Company \u57DF\u540D", value: domain, placeholder: "company.example", onChange: event => setDomain(event.target.value), disabled: busy || view.status.state !== 'ready' })] }), _jsx("button", { disabled: busy || !domain.trim() || view.status.state !== 'ready', children: "\u53D1\u73B0 Company" })] }), view.status.companies.map(company => _jsxs("article", { style: { border: '1px solid #8885', padding: 12 }, children: [_jsx("h3", { children: company.name }), _jsx("p", { children: company.authMode === 'no_as' ? '无需账户即可使用 Session' : company.authMode === 'disabled' ? '暂停新账户登录,已有账户按授权继续使用' : '支持独立账户登录' }), _jsx("button", { disabled: busy, onClick: () => run({ action: 'session/connect', companyRef: company.companyRef }), children: "\u5EFA\u7ACB\u672A\u767B\u5F55\u8FDE\u63A5" }), view.status.sessions.filter(session => session.companyRef === company.companyRef && session.connected).map(session => _jsxs("p", { children: [session.account ? '账户连接' : '未登录连接', " ", session.sessionRef.slice(-8), " ", _jsxs("button", { disabled: busy, onClick: () => run({ action: 'session/disconnect', sessionRef: session.sessionRef }), children: ["\u65AD\u5F00\u8FDE\u63A5 ", session.sessionRef.slice(-8)] })] }, session.sessionRef)), view.status.accounts.filter(account => account.companyRef === company.companyRef).map(account => _jsxs("div", { children: [_jsxs("span", { children: ["\u8D26\u6237 ", account.accountSlot.slice(-8), " \u00B7 ", account.status, " \u00B7 ", account.scopes.join('、')] }), account.status === 'active' && _jsx("button", { disabled: busy, onClick: () => run({ action: 'session/connect', companyRef: company.companyRef, accountSlot: account.accountSlot }), children: "\u5EFA\u7ACB\u8D26\u6237\u8FDE\u63A5" }), account.status === 'active' && _jsx("button", { disabled: busy || !account.canRefresh, onClick: () => run({ action: 'account/refresh', accountSlot: account.accountSlot }), children: "\u5237\u65B0\u8D26\u6237" }), !['signed_out'].includes(account.status) && _jsx("button", { disabled: busy, onClick: () => run({ action: 'account/sign-out', accountSlot: account.accountSlot }), children: "\u9000\u51FA\u8D26\u6237" })] }, account.accountSlot)), view.operations.filter(operation => operation.companyRef === company.companyRef).map(operation => _jsx(Operation, { operation: operation, company: company, view: view, controller: controller }, operation.operationRef))] }, company.companyRef)), view.status.pendingSignIns.map(pending => _jsxs("div", { children: [_jsx("p", { children: "\u8D26\u6237\u767B\u5F55\u7B49\u5F85\u5B8C\u6210\u3002" }), view.authorization?.transactionRef === pending.transactionRef && _jsx("a", { href: view.authorization.url, target: "_blank", rel: "noopener noreferrer", children: "\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u8D26\u6237\u767B\u5F55" }), _jsx("button", { disabled: busy, onClick: () => run({ action: 'account/collect', transactionRef: pending.transactionRef }), children: "\u9886\u53D6\u767B\u5F55\u7ED3\u679C" }), _jsx("button", { disabled: busy, onClick: () => run({ action: 'account/sign-out', accountSlot: pending.accountSlot }), children: "\u53D6\u6D88\u8D26\u6237\u767B\u5F55" })] }, pending.transactionRef))] });
|
|
20
|
+
}
|
|
21
|
+
function Operation({ operation, company, view, controller }) {
|
|
22
|
+
const [sessionRef, setSession] = useState('');
|
|
23
|
+
const [scopes, setScopes] = useState([]);
|
|
24
|
+
const sessions = view.status.sessions.filter(session => session.companyRef === company.companyRef && session.connected);
|
|
25
|
+
const selected = sessions.find(session => session.sessionRef === sessionRef);
|
|
26
|
+
const busy = view.pending || view.loading;
|
|
27
|
+
return _jsxs("fieldset", { children: [_jsxs("legend", { children: [operation.method, " ", operation.path] }), _jsx("p", { children: operation.enabled ? '已允许 Agent 调用' : '未允许 Agent 调用' }), _jsxs("label", { children: ["\u64CD\u4F5C\u8FDE\u63A5 ", _jsxs("select", { "aria-label": `连接 ${operation.method} ${operation.path}`, value: sessionRef, onChange: event => setSession(event.target.value), disabled: busy, children: [_jsx("option", { value: "", children: "\u9009\u62E9\u8FDE\u63A5" }), sessions.map(session => _jsxs("option", { value: session.sessionRef, children: [session.account ? '账户' : '未登录', " ", session.sessionRef.slice(-8)] }, session.sessionRef))] })] }), !!company.scopes.length && _jsxs("div", { children: ["\u8D26\u6237\u6743\u9650 ", company.scopes.map(scope => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: scopes.includes(scope), disabled: busy, onChange: event => setScopes(event.target.checked ? [...scopes, scope] : scopes.filter(item => item !== scope)) }), scope] }, scope))] }), _jsx("button", { disabled: busy || !selected, onClick: () => { void controller.execute({ action: 'operation/enable', operationRef: operation.operationRef, sessionRef, scopes: selected?.account ? scopes : [] }); }, children: "\u5141\u8BB8 Agent \u8C03\u7528" }), operation.enabled && _jsx("button", { disabled: busy, onClick: () => { void controller.execute({ action: 'operation/disable', operationRef: operation.operationRef }); }, children: "\u64A4\u9500\u64CD\u4F5C\u8BB8\u53EF" }), _jsx("button", { disabled: busy || !selected || scopes.length === 0 || company.authMode !== 'account', onClick: () => { void controller.execute({ action: 'account/sign-in', sessionRef, operationRef: operation.operationRef, scopes }); }, children: "\u53D1\u8D77\u8D26\u6237\u767B\u5F55" })] });
|
|
28
|
+
}
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { CredentialObservation } from './credential-history.js';
|
|
2
|
+
import type { PapHostOptions } from './host.js';
|
|
3
|
+
import { PapStore } from './store.js';
|
|
4
|
+
import type { Company, Grant, HttpResponse, HttpTransport, Operation, PapState } from './types.js';
|
|
5
|
+
/** Same-process permission fence, never a wire input. */
|
|
6
|
+
export interface PapDispatchControl {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
assertAllowed?: (state: PapState) => void;
|
|
9
|
+
/** Host-only in-memory response redaction set; never register this callback on a wire surface. */
|
|
10
|
+
captureSecrets?: (values: readonly string[], credentials?: CredentialObservation) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Host-only engine. UI receives safe summaries, never this object or signer capabilities. */
|
|
13
|
+
export declare class PapClient {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private readonly transport;
|
|
16
|
+
readonly store: PapStore;
|
|
17
|
+
private readonly discovery;
|
|
18
|
+
private readonly clock;
|
|
19
|
+
private closed;
|
|
20
|
+
private readonly discoveryFlights;
|
|
21
|
+
private readonly recentJtis;
|
|
22
|
+
private readonly cooldownWaiters;
|
|
23
|
+
private readonly refreshFlights;
|
|
24
|
+
private readonly abortedSessions;
|
|
25
|
+
constructor(options: PapHostOptions, transport: HttpTransport);
|
|
26
|
+
close(): void;
|
|
27
|
+
recover(): Promise<void>;
|
|
28
|
+
resolvePapCompany(domain: string): Promise<{
|
|
29
|
+
companyRef: string;
|
|
30
|
+
companyId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
authMode: Company['authMode'];
|
|
33
|
+
scopes: string[];
|
|
34
|
+
}>;
|
|
35
|
+
getOrCreatePapUser(companyRef: string, mode?: 'regular' | 'anonymous', agent?: boolean): Promise<{
|
|
36
|
+
subjectRef: string;
|
|
37
|
+
}>;
|
|
38
|
+
openPapSession(subjectRef: string, accountSlot?: string): Promise<{
|
|
39
|
+
sessionRef: string;
|
|
40
|
+
}>;
|
|
41
|
+
private company;
|
|
42
|
+
private subject;
|
|
43
|
+
private sameUser;
|
|
44
|
+
private assertAccountEpoch;
|
|
45
|
+
private session;
|
|
46
|
+
private ensureCompanyFresh;
|
|
47
|
+
private lease;
|
|
48
|
+
private assertWaitActive;
|
|
49
|
+
private awaitCooldown;
|
|
50
|
+
rotateDelegation(subjectRef: string): Promise<void>;
|
|
51
|
+
private delegation;
|
|
52
|
+
private cleanupRetirements;
|
|
53
|
+
dispatchPap(sessionRef: string, operation: Operation, control?: PapDispatchControl): Promise<HttpResponse>;
|
|
54
|
+
private checkSessionJwt;
|
|
55
|
+
private saveNonce;
|
|
56
|
+
startSignIn(subjectRef: string, input: {
|
|
57
|
+
scopes: string[];
|
|
58
|
+
resource: string;
|
|
59
|
+
redirectUri: string;
|
|
60
|
+
callbackMode: 'handoff' | 'loopback';
|
|
61
|
+
method?: 'direct' | 'device';
|
|
62
|
+
}): Promise<{
|
|
63
|
+
transactionRef: string;
|
|
64
|
+
authorizationUrl: string;
|
|
65
|
+
accountSlot: string;
|
|
66
|
+
}>;
|
|
67
|
+
collectSignIn(transactionRef: string): Promise<{
|
|
68
|
+
grantRef: string;
|
|
69
|
+
accountSlot: string;
|
|
70
|
+
}>;
|
|
71
|
+
/** Called only by an authenticated Host callback listener or handoff, never an arbitrary model tool. */
|
|
72
|
+
completeSignIn(transactionRef: string, response: {
|
|
73
|
+
state?: string;
|
|
74
|
+
iss?: string;
|
|
75
|
+
code?: string;
|
|
76
|
+
error?: string;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
grantRef: string;
|
|
79
|
+
accountSlot: string;
|
|
80
|
+
}>;
|
|
81
|
+
private parseGrant;
|
|
82
|
+
private tokenRequest;
|
|
83
|
+
refreshAccountGrant(grantRef: string, sessionRef?: string): Promise<void>;
|
|
84
|
+
private waitForRefresh;
|
|
85
|
+
private refresh;
|
|
86
|
+
signOutAccount(subjectRef: string, accountSlot: string): Promise<{
|
|
87
|
+
status: 'signed_out' | 'revocation_pending';
|
|
88
|
+
}>;
|
|
89
|
+
disconnectPap(sessionRef: string): Promise<void>;
|
|
90
|
+
getPapStatus(): Promise<{
|
|
91
|
+
companies: {
|
|
92
|
+
companyRef: string;
|
|
93
|
+
name: string;
|
|
94
|
+
authMode: Company['authMode'];
|
|
95
|
+
}[];
|
|
96
|
+
sessions: {
|
|
97
|
+
sessionRef: string;
|
|
98
|
+
connected: boolean;
|
|
99
|
+
account: boolean;
|
|
100
|
+
}[];
|
|
101
|
+
accounts: {
|
|
102
|
+
grantRef: string;
|
|
103
|
+
status: Grant['status'];
|
|
104
|
+
scopes: string[];
|
|
105
|
+
}[];
|
|
106
|
+
supportedSignIn: ['direct'];
|
|
107
|
+
}>;
|
|
108
|
+
}
|