@poppykit/server 0.3.1 → 0.5.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/API.md +11 -3
- package/AUTH.md +120 -95
- package/CHANGELOG.md +18 -0
- package/CONFORMANCE.md +73 -0
- package/CONTRIBUTING.md +13 -10
- package/DELIVERY.md +21 -1
- package/INTEGRATIONS.md +72 -69
- package/OPERATIONS.md +142 -0
- package/README.md +14 -17
- package/RELEASING.md +2 -0
- package/SECURITY.md +2 -0
- package/STORAGE.md +57 -2
- package/dist/adapter-contract.d.ts +18 -0
- package/dist/adapter-contract.js +90 -11
- package/dist/auth/authenticator.d.ts +3 -1
- package/dist/auth/authenticator.js +34 -29
- package/dist/auth/authority.d.ts +8 -2
- package/dist/auth/authority.js +7 -2
- package/dist/auth/session.d.ts +47 -0
- package/dist/auth/session.js +94 -0
- package/dist/browser.d.ts +15 -13
- package/dist/browser.js +80 -46
- package/dist/conversation/service.d.ts +18 -8
- package/dist/conversation/service.js +60 -28
- package/dist/feedback.d.ts +1 -1
- package/dist/feedback.js +5 -5
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +8 -4
- package/dist/network/discovery.d.ts +51 -22
- package/dist/network/discovery.js +159 -69
- package/dist/network/redirect.d.ts +3 -0
- package/dist/network/redirect.js +38 -0
- package/dist/network/tasks.d.ts +23 -0
- package/dist/network/tasks.js +45 -0
- package/dist/operations.d.ts +115 -0
- package/dist/operations.js +212 -0
- package/dist/postgres.d.ts +6 -0
- package/dist/postgres.js +92 -6
- package/dist/protocol/operations.d.ts +51 -0
- package/dist/protocol/operations.js +98 -0
- package/dist/protocol/session.d.ts +28 -0
- package/dist/protocol/session.js +93 -0
- package/dist/protocol/sse.d.ts +1 -0
- package/dist/protocol/sse.js +1 -1
- package/dist/protocol/wire.d.ts +20 -15
- package/dist/protocol/wire.js +70 -22
- package/dist/reference-auth/index.d.ts +17 -12
- package/dist/reference-auth/index.js +252 -386
- package/dist/reference-auth/store.d.ts +2 -1
- package/dist/sqlite.d.ts +11 -0
- package/dist/sqlite.js +104 -6
- package/examples/operations.mjs +18 -0
- package/examples/quickstart.mjs +10 -6
- package/migrations/RETIREMENT.md +77 -0
- package/package.json +5 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MetadataResolver } from '../network/discovery.js';
|
|
2
|
+
export interface SessionClaims {
|
|
3
|
+
iss: string;
|
|
4
|
+
aud: string;
|
|
5
|
+
sid: string;
|
|
6
|
+
sub: string;
|
|
7
|
+
agent_id?: string;
|
|
8
|
+
htm: string;
|
|
9
|
+
htu: string;
|
|
10
|
+
iat: number;
|
|
11
|
+
jti: string;
|
|
12
|
+
ath?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface VerifiedSession extends SessionClaims {
|
|
15
|
+
delegationExpiresAt: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function sessionRequestUrl(value: string): string;
|
|
18
|
+
export declare function parseSessionClaims(value: unknown): SessionClaims;
|
|
19
|
+
/** Verifies identity only. The resource must separately admit/continue the client_id + sid under its store fence. */
|
|
20
|
+
export declare function verifySessionJwt(jwt: string | null, expected: {
|
|
21
|
+
companyId: string;
|
|
22
|
+
method: string;
|
|
23
|
+
url: string;
|
|
24
|
+
nowMs: number;
|
|
25
|
+
metadata: MetadataResolver;
|
|
26
|
+
accessToken?: string;
|
|
27
|
+
tokenClientId?: string;
|
|
28
|
+
}): Promise<VerifiedSession>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { calculateJwkThumbprint, compactVerify, createLocalJWKSet, decodeProtectedHeader, errors, importJWK } from 'jose';
|
|
3
|
+
import { id, object, parseJson, string, ValidationError } from './wire.js';
|
|
4
|
+
import { validateClientId } from '../network/discovery.js';
|
|
5
|
+
import { secureUrl } from '../network/transport.js';
|
|
6
|
+
const COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
7
|
+
const PRIVATE = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'];
|
|
8
|
+
const hash = (value) => createHash('sha256').update(value).digest('base64url');
|
|
9
|
+
function unique(jwt) {
|
|
10
|
+
if (!COMPACT.test(jwt))
|
|
11
|
+
throw new ValidationError('session.jwt');
|
|
12
|
+
for (const segment of jwt.split('.').slice(0, 2))
|
|
13
|
+
parseJson(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
14
|
+
}
|
|
15
|
+
function timestamp(value) {
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
17
|
+
throw new ValidationError('session.time');
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function bounded(value, field) {
|
|
21
|
+
const result = string(value, field);
|
|
22
|
+
if (result.length > 256)
|
|
23
|
+
throw new ValidationError(field);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export function sessionRequestUrl(value) {
|
|
27
|
+
const url = secureUrl(value);
|
|
28
|
+
return `${url.origin}${url.pathname}`;
|
|
29
|
+
}
|
|
30
|
+
export function parseSessionClaims(value) {
|
|
31
|
+
const claims = object(value, 'session');
|
|
32
|
+
validateClientId(string(claims.iss, 'session.iss'));
|
|
33
|
+
secureUrl(string(claims.aud, 'session.aud'));
|
|
34
|
+
id(claims.sid, 'session.sid');
|
|
35
|
+
bounded(claims.sub, 'session.sub');
|
|
36
|
+
if ('agent_id' in claims)
|
|
37
|
+
bounded(claims.agent_id, 'session.agent_id');
|
|
38
|
+
string(claims.htm, 'session.htm');
|
|
39
|
+
const htu = string(claims.htu, 'session.htu');
|
|
40
|
+
if (htu.includes('?') || htu.includes('#'))
|
|
41
|
+
throw new ValidationError('session.htu');
|
|
42
|
+
secureUrl(htu);
|
|
43
|
+
timestamp(claims.iat);
|
|
44
|
+
const jti = bounded(claims.jti, 'session.jti');
|
|
45
|
+
if (jti.length < 16)
|
|
46
|
+
throw new ValidationError('session.jti');
|
|
47
|
+
if ('ath' in claims)
|
|
48
|
+
string(claims.ath, 'session.ath');
|
|
49
|
+
return claims;
|
|
50
|
+
}
|
|
51
|
+
/** Verifies identity only. The resource must separately admit/continue the client_id + sid under its store fence. */
|
|
52
|
+
export async function verifySessionJwt(jwt, expected) {
|
|
53
|
+
if (!jwt || jwt.length > 32768)
|
|
54
|
+
throw new ValidationError('session.jwt');
|
|
55
|
+
unique(jwt);
|
|
56
|
+
const header = decodeProtectedHeader(jwt);
|
|
57
|
+
if (header.typ !== 'poppy-session+jwt' || header.alg !== 'ES256' || typeof header.kid !== 'string' || typeof header.pap_dlg !== 'string')
|
|
58
|
+
throw new ValidationError('session.header');
|
|
59
|
+
const claims = parseSessionClaims(parseJson(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8')));
|
|
60
|
+
const delegation = header.pap_dlg;
|
|
61
|
+
unique(delegation);
|
|
62
|
+
const delegatedHeader = decodeProtectedHeader(delegation);
|
|
63
|
+
if (delegatedHeader.typ !== 'pap-delegation+jwt' || typeof delegatedHeader.alg !== 'string' || !['ES256', 'PS384'].includes(delegatedHeader.alg) || typeof delegatedHeader.kid !== 'string')
|
|
64
|
+
throw new ValidationError('delegation.header');
|
|
65
|
+
const client = await expected.metadata.resolveClient(claims.iss);
|
|
66
|
+
let jwks = await expected.metadata.resolveJwks(client.jwks_uri);
|
|
67
|
+
let verified;
|
|
68
|
+
try {
|
|
69
|
+
verified = await compactVerify(delegation, createLocalJWKSet(jwks), { algorithms: ['ES256', 'PS384'] });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(error instanceof errors.JWKSNoMatchingKey))
|
|
73
|
+
throw error;
|
|
74
|
+
jwks = await expected.metadata.resolveJwks(client.jwks_uri, { refresh: true });
|
|
75
|
+
verified = await compactVerify(delegation, createLocalJWKSet(jwks), { algorithms: ['ES256', 'PS384'] });
|
|
76
|
+
}
|
|
77
|
+
const delegated = object(parseJson(new TextDecoder().decode(verified.payload)), 'delegation');
|
|
78
|
+
const issued = timestamp(delegated.iat), expires = timestamp(delegated.exp), at = expected.nowMs / 1000;
|
|
79
|
+
bounded(delegated.jti, 'delegation.jti');
|
|
80
|
+
if (delegated.jti.length < 16 || delegated.iss !== claims.iss || expires <= issued || expires - issued > 86400 || expires <= at || issued > at + 30)
|
|
81
|
+
throw new ValidationError('delegation.claims');
|
|
82
|
+
const key = object(object(delegated.cnf, 'delegation.cnf').jwk, 'delegation.key');
|
|
83
|
+
if (key.kty !== 'EC' || key.crv !== 'P-256' || PRIVATE.some(name => name in key) || typeof key.x !== 'string' || typeof key.y !== 'string')
|
|
84
|
+
throw new ValidationError('delegation.key');
|
|
85
|
+
if (header.kid !== await calculateJwkThumbprint(key) || claims.iat > expires)
|
|
86
|
+
throw new ValidationError('session.key');
|
|
87
|
+
await compactVerify(jwt, await importJWK(key, 'ES256'), { algorithms: ['ES256'] });
|
|
88
|
+
if (claims.aud !== expected.companyId || claims.htm !== expected.method || sessionRequestUrl(claims.htu) !== sessionRequestUrl(expected.url) || claims.iat < at - 60 || claims.iat > at + 30)
|
|
89
|
+
throw new ValidationError('session.binding');
|
|
90
|
+
if (expected.accessToken === undefined ? 'ath' in claims : claims.ath !== hash(expected.accessToken) || expected.tokenClientId !== claims.iss)
|
|
91
|
+
throw new ValidationError('session.token');
|
|
92
|
+
return { ...claims, delegationExpiresAt: expires * 1000 };
|
|
93
|
+
}
|
package/dist/protocol/sse.d.ts
CHANGED
package/dist/protocol/sse.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function* parseEventStream(body, options = {}) {
|
|
|
18
18
|
const delta = object(value);
|
|
19
19
|
return { kind: 'delta', messageId: id(delta.message_id, 'message_id'), text: string(delta.text, 'text', false) };
|
|
20
20
|
}
|
|
21
|
-
const event = parseEvent(value);
|
|
21
|
+
const event = parseEvent(value, options.operations);
|
|
22
22
|
if (eventId !== event.id)
|
|
23
23
|
throw new ValidationError('sse.id');
|
|
24
24
|
return { kind: 'event', event };
|
package/dist/protocol/wire.d.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import type { BusinessOperation, OperationApproval } from './operations.js';
|
|
1
2
|
export type Json = null | boolean | number | string | Json[] | {
|
|
2
3
|
[key: string]: Json;
|
|
3
4
|
};
|
|
4
5
|
export type JsonObject = {
|
|
5
6
|
[key: string]: Json;
|
|
6
7
|
};
|
|
7
|
-
export type
|
|
8
|
+
export type AuthorType = 'agent' | 'human';
|
|
9
|
+
export interface Author {
|
|
10
|
+
type: AuthorType;
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
image?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
8
16
|
export type Status = 'working' | 'idle' | 'queued' | 'closed';
|
|
9
17
|
export interface Message {
|
|
10
18
|
id: string;
|
|
11
|
-
|
|
19
|
+
author: Author;
|
|
12
20
|
text?: string;
|
|
13
21
|
data?: JsonObject;
|
|
14
22
|
context?: JsonObject;
|
|
23
|
+
operation?: BusinessOperation;
|
|
24
|
+
approval?: OperationApproval;
|
|
15
25
|
[key: string]: unknown;
|
|
16
26
|
}
|
|
17
27
|
export interface RecordedMessage extends Message {
|
|
@@ -25,7 +35,7 @@ export interface ConversationRequest {
|
|
|
25
35
|
export interface ConversationState {
|
|
26
36
|
conversation_id: string;
|
|
27
37
|
status: Status;
|
|
28
|
-
responder:
|
|
38
|
+
responder: AuthorType;
|
|
29
39
|
[key: string]: unknown;
|
|
30
40
|
}
|
|
31
41
|
export interface ConversationEvent {
|
|
@@ -40,11 +50,7 @@ export type KnownEvent = (ConversationEvent & {
|
|
|
40
50
|
}) | (ConversationEvent & {
|
|
41
51
|
type: 'state';
|
|
42
52
|
status: Status;
|
|
43
|
-
responder:
|
|
44
|
-
}) | (ConversationEvent & {
|
|
45
|
-
type: 'authorization';
|
|
46
|
-
error: 'sign_in_required';
|
|
47
|
-
scope?: string;
|
|
53
|
+
responder: AuthorType;
|
|
48
54
|
}) | (ConversationEvent & {
|
|
49
55
|
type: 'authorization';
|
|
50
56
|
error: 'insufficient_scope';
|
|
@@ -64,10 +70,8 @@ export interface EventPage extends ConversationState {
|
|
|
64
70
|
export interface TokenResponse {
|
|
65
71
|
access_token: string;
|
|
66
72
|
token_type: 'DPoP' | 'Bearer';
|
|
67
|
-
expires_in
|
|
68
|
-
scope
|
|
69
|
-
session_id: string;
|
|
70
|
-
signed_in: boolean;
|
|
73
|
+
expires_in?: number;
|
|
74
|
+
scope?: string;
|
|
71
75
|
refresh_token?: string;
|
|
72
76
|
refresh_token_expires_in?: number;
|
|
73
77
|
[key: string]: unknown;
|
|
@@ -92,11 +96,12 @@ export declare function id(value: unknown, field: string): string;
|
|
|
92
96
|
export declare function parseMessage(value: unknown, options?: {
|
|
93
97
|
recorded?: boolean;
|
|
94
98
|
company?: boolean;
|
|
99
|
+
operations?: boolean;
|
|
95
100
|
}): Message | RecordedMessage;
|
|
96
|
-
export declare function parseConversationRequest(value: unknown): ConversationRequest;
|
|
101
|
+
export declare function parseConversationRequest(value: unknown, operations?: boolean): ConversationRequest;
|
|
97
102
|
export declare function parseConversationState(value: unknown): ConversationState;
|
|
98
|
-
export declare function parseEvent(value: unknown): ConversationEvent;
|
|
99
|
-
export declare function parseEventPage(value: unknown, requestCursor?: string): EventPage;
|
|
103
|
+
export declare function parseEvent(value: unknown, operations?: boolean): ConversationEvent;
|
|
104
|
+
export declare function parseEventPage(value: unknown, requestCursor?: string, operations?: boolean): EventPage;
|
|
100
105
|
export declare function parseTokenResponse(value: unknown): TokenResponse;
|
|
101
106
|
export declare function parseProtocolError(value: unknown): ProtocolErrorBody;
|
|
102
107
|
export declare function parseReadOptions(url: URL, maxWaitSeconds: number): {
|
package/dist/protocol/wire.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseBusinessOperation, parseOperationApproval } from './operations.js';
|
|
1
2
|
export class ValidationError extends Error {
|
|
2
3
|
field;
|
|
3
4
|
constructor(field) {
|
|
@@ -50,8 +51,38 @@ function positive(value, field) {
|
|
|
50
51
|
export function parseMessage(value, options = {}) {
|
|
51
52
|
const message = { ...object(value, 'message') };
|
|
52
53
|
id(message.id, 'message.id');
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const author = object(message.author, 'message.author');
|
|
55
|
+
member(author.type, ['agent', 'human'], 'message.author.type');
|
|
56
|
+
if ('sender' in message)
|
|
57
|
+
throw new ValidationError('message.sender');
|
|
58
|
+
for (const key of ['id', 'name', 'image'])
|
|
59
|
+
if (key in author) {
|
|
60
|
+
if (!options.company && !(options.recorded && message.role === 'company'))
|
|
61
|
+
throw new ValidationError('message.author.' + key);
|
|
62
|
+
string(author[key], 'message.author.' + key);
|
|
63
|
+
}
|
|
64
|
+
if ('image' in author) {
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(author.image);
|
|
67
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
68
|
+
throw 0;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new ValidationError('message.author.image');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const company = options.company || (options.recorded && message.role === 'company');
|
|
75
|
+
if ('operation' in message) {
|
|
76
|
+
if (!company || !options.operations)
|
|
77
|
+
throw new ValidationError('message.operation');
|
|
78
|
+
message.operation = parseBusinessOperation(message.operation);
|
|
79
|
+
}
|
|
80
|
+
if ('approval' in message) {
|
|
81
|
+
if (company || !options.operations)
|
|
82
|
+
throw new ValidationError('message.approval');
|
|
83
|
+
message.approval = parseOperationApproval(message.approval);
|
|
84
|
+
}
|
|
85
|
+
if (!['text', 'data', 'context', ...(options.operations ? [company ? 'operation' : 'approval'] : [])].some(key => key in message))
|
|
55
86
|
throw new ValidationError('message.content');
|
|
56
87
|
if ('text' in message)
|
|
57
88
|
string(message.text, 'message.text', false);
|
|
@@ -90,20 +121,24 @@ export function parseMessage(value, options = {}) {
|
|
|
90
121
|
}
|
|
91
122
|
return message;
|
|
92
123
|
}
|
|
93
|
-
export function parseConversationRequest(value) {
|
|
124
|
+
export function parseConversationRequest(value, operations = false) {
|
|
94
125
|
const request = object(value);
|
|
95
126
|
if ('parent_conversation_id' in request)
|
|
96
127
|
id(request.parent_conversation_id, 'parent_conversation_id');
|
|
97
|
-
return { ...request, message: parseMessage(request.message) };
|
|
128
|
+
return { ...request, message: parseMessage(request.message, { operations }) };
|
|
98
129
|
}
|
|
99
130
|
export function parseConversationState(value) {
|
|
100
131
|
const state = object(value);
|
|
101
132
|
id(state.conversation_id, 'conversation_id');
|
|
102
|
-
|
|
103
|
-
|
|
133
|
+
string(state.status, 'status');
|
|
134
|
+
string(state.responder, 'responder');
|
|
135
|
+
if (!['working', 'idle', 'queued', 'closed'].includes(state.status))
|
|
136
|
+
state.status = 'working';
|
|
137
|
+
if (!['agent', 'human'].includes(state.responder))
|
|
138
|
+
state.responder = 'human';
|
|
104
139
|
return state;
|
|
105
140
|
}
|
|
106
|
-
export function parseEvent(value) {
|
|
141
|
+
export function parseEvent(value, operations = false) {
|
|
107
142
|
const event = object(value, 'event');
|
|
108
143
|
id(event.id, 'event.id');
|
|
109
144
|
string(event.type, 'event.type');
|
|
@@ -119,16 +154,18 @@ export function parseEvent(value) {
|
|
|
119
154
|
throw new ValidationError('event.created_at');
|
|
120
155
|
switch (event.type) {
|
|
121
156
|
case 'message':
|
|
122
|
-
parseMessage(event.message, { recorded: true });
|
|
157
|
+
parseMessage(event.message, { recorded: true, operations });
|
|
123
158
|
break;
|
|
124
|
-
case 'state':
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
case 'state': {
|
|
160
|
+
const state = parseConversationState({ conversation_id: 'state', status: event.status, responder: event.responder });
|
|
161
|
+
event.status = state.status;
|
|
162
|
+
event.responder = state.responder;
|
|
127
163
|
break;
|
|
164
|
+
}
|
|
128
165
|
case 'authorization':
|
|
129
|
-
member(event.error, ['
|
|
130
|
-
if (event.
|
|
131
|
-
|
|
166
|
+
member(event.error, ['insufficient_scope'], 'event.error');
|
|
167
|
+
if (!/^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/.test(string(event.scope, 'event.scope')))
|
|
168
|
+
throw new ValidationError('event.scope');
|
|
132
169
|
break;
|
|
133
170
|
case 'user_requested':
|
|
134
171
|
string(event.reason, 'event.reason', false);
|
|
@@ -140,11 +177,11 @@ export function parseEvent(value) {
|
|
|
140
177
|
}
|
|
141
178
|
return event;
|
|
142
179
|
}
|
|
143
|
-
export function parseEventPage(value, requestCursor) {
|
|
180
|
+
export function parseEventPage(value, requestCursor, operations = false) {
|
|
144
181
|
const page = parseConversationState(value);
|
|
145
182
|
if (!Array.isArray(page.events))
|
|
146
183
|
throw new ValidationError('events');
|
|
147
|
-
page.events.forEach(parseEvent);
|
|
184
|
+
page.events.forEach(event => parseEvent(event, operations));
|
|
148
185
|
member(page.has_more, [true, false], 'has_more');
|
|
149
186
|
if (page.cursor === null) {
|
|
150
187
|
if (requestCursor !== undefined || page.events.length || page.has_more)
|
|
@@ -153,7 +190,7 @@ export function parseEventPage(value, requestCursor) {
|
|
|
153
190
|
else {
|
|
154
191
|
string(page.cursor, 'cursor');
|
|
155
192
|
const expected = page.events.length ? page.events.at(-1).id : requestCursor;
|
|
156
|
-
if (expected
|
|
193
|
+
if (expected === undefined || page.cursor !== expected)
|
|
157
194
|
throw new ValidationError('cursor');
|
|
158
195
|
}
|
|
159
196
|
return page;
|
|
@@ -161,11 +198,17 @@ export function parseEventPage(value, requestCursor) {
|
|
|
161
198
|
export function parseTokenResponse(value) {
|
|
162
199
|
const token = object(value);
|
|
163
200
|
string(token.access_token, 'access_token');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
201
|
+
if ('scope' in token) {
|
|
202
|
+
const scope = string(token.scope, 'scope', false);
|
|
203
|
+
if (scope !== '' && (!/^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/.test(scope) || new Set(scope.split(' ')).size !== scope.split(' ').length))
|
|
204
|
+
throw new ValidationError('scope');
|
|
205
|
+
}
|
|
206
|
+
const type = string(token.token_type, 'token_type').toLowerCase();
|
|
207
|
+
if (type !== 'bearer' && type !== 'dpop')
|
|
208
|
+
throw new ValidationError('token_type');
|
|
209
|
+
token.token_type = type === 'bearer' ? 'Bearer' : 'DPoP';
|
|
210
|
+
if ('expires_in' in token && (!Number.isSafeInteger(token.expires_in) || token.expires_in < 0))
|
|
211
|
+
throw new ValidationError('expires_in');
|
|
169
212
|
if ('refresh_token' in token)
|
|
170
213
|
string(token.refresh_token, 'refresh_token');
|
|
171
214
|
if ('refresh_token_expires_in' in token)
|
|
@@ -215,6 +258,11 @@ export function parseAssertionClaims(value) {
|
|
|
215
258
|
const claims = object(value, 'claims');
|
|
216
259
|
for (const key of ['iss', 'sub', 'aud', 'jti'])
|
|
217
260
|
string(claims[key], key);
|
|
261
|
+
if ('agent_id' in claims) {
|
|
262
|
+
string(claims.agent_id, 'agent_id');
|
|
263
|
+
if (claims.agent_id.length > 256)
|
|
264
|
+
throw new ValidationError('agent_id');
|
|
265
|
+
}
|
|
218
266
|
// 128 random bits is at least 16 bytes; cap length since jti lands in the replay store. Encoding is not constrained.
|
|
219
267
|
if (claims.jti.length < 16 || claims.jti.length > 256)
|
|
220
268
|
throw new ValidationError('jti');
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { MetadataResolver } from '../network/discovery.js';
|
|
2
2
|
import type { IssuerStore, IssuerSelection } from './store.js';
|
|
3
3
|
import type { AuthorityCoordinator, ExternalAuthorityAdapter } from '../auth/authority.js';
|
|
4
|
-
import type { BrowserSessionAuthority } from '../browser.js';
|
|
5
4
|
export type { IssuerStore, IssuerSelection };
|
|
6
5
|
export { createMetadataResolver } from '../network/discovery.js';
|
|
7
6
|
export { createNodeTransport } from '../network/transport.js';
|
|
8
7
|
interface SessionRecord {
|
|
9
8
|
clientId: string;
|
|
10
9
|
userId: string;
|
|
10
|
+
agentId?: string;
|
|
11
11
|
version: number;
|
|
12
12
|
createdAt: number;
|
|
13
13
|
expiresAt: number;
|
|
@@ -25,6 +25,8 @@ interface GrantRecord {
|
|
|
25
25
|
previousHash: string;
|
|
26
26
|
}
|
|
27
27
|
interface AccessTokenRecord {
|
|
28
|
+
clientId?: string;
|
|
29
|
+
accountId?: string;
|
|
28
30
|
sessionId: string;
|
|
29
31
|
grantId: string | null;
|
|
30
32
|
scopes: string[];
|
|
@@ -33,6 +35,7 @@ interface AccessTokenRecord {
|
|
|
33
35
|
expiresAt: number;
|
|
34
36
|
}
|
|
35
37
|
interface AuthorizationRecord {
|
|
38
|
+
sessionKey: string | null;
|
|
36
39
|
clientId: string;
|
|
37
40
|
redirectUri: string;
|
|
38
41
|
state: string;
|
|
@@ -47,6 +50,7 @@ interface AuthorizationRecord {
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
interface CodeRecord {
|
|
53
|
+
sessionKey: string | null;
|
|
50
54
|
clientId: string;
|
|
51
55
|
redirectUri: string;
|
|
52
56
|
codeChallenge: string;
|
|
@@ -58,6 +62,7 @@ interface CodeRecord {
|
|
|
58
62
|
}
|
|
59
63
|
export type SignInRecord = {
|
|
60
64
|
kind: 'device';
|
|
65
|
+
sessionKey: string | null;
|
|
61
66
|
clientId: string;
|
|
62
67
|
scopes: string[];
|
|
63
68
|
expiresAt: number;
|
|
@@ -70,9 +75,11 @@ export type SignInRecord = {
|
|
|
70
75
|
accountId?: string;
|
|
71
76
|
} | {
|
|
72
77
|
kind: 'mediated';
|
|
78
|
+
resource?: string | null;
|
|
79
|
+
grantId?: string;
|
|
73
80
|
clientId: string;
|
|
74
|
-
userId
|
|
75
|
-
sessionId
|
|
81
|
+
userId?: string;
|
|
82
|
+
sessionId?: string;
|
|
76
83
|
scopes: string[];
|
|
77
84
|
expiresAt: number;
|
|
78
85
|
status: 'verifying' | 'pending' | 'failed' | 'expired' | 'complete';
|
|
@@ -97,12 +104,10 @@ export type SignInRecord = {
|
|
|
97
104
|
};
|
|
98
105
|
export interface MediatedContext {
|
|
99
106
|
clientId: string;
|
|
100
|
-
userId: string;
|
|
101
|
-
sessionId: string;
|
|
102
107
|
issuer: string;
|
|
103
108
|
}
|
|
104
109
|
export interface MediatedOptions {
|
|
105
|
-
endpoint
|
|
110
|
+
endpoint?: string;
|
|
106
111
|
scopes: string[];
|
|
107
112
|
fields: {
|
|
108
113
|
name: string;
|
|
@@ -132,9 +137,10 @@ export interface IssuerState {
|
|
|
132
137
|
pending: number;
|
|
133
138
|
pendingForClient: number;
|
|
134
139
|
};
|
|
135
|
-
version: 3;
|
|
140
|
+
version: 3 | 4 | 5;
|
|
136
141
|
generation: string;
|
|
137
142
|
sessions: Record<string, SessionRecord>;
|
|
143
|
+
sessionKeys: Record<string, string>;
|
|
138
144
|
grants: Record<string, GrantRecord>;
|
|
139
145
|
refreshTokens: Record<string, string>;
|
|
140
146
|
authorizations: Record<string, AuthorizationRecord>;
|
|
@@ -152,8 +158,8 @@ export interface ReferenceIssuerOptions {
|
|
|
152
158
|
tokenEndpoint: string;
|
|
153
159
|
revocationEndpoint: string;
|
|
154
160
|
authorizationEndpoint?: string;
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
protectedResources: string[];
|
|
162
|
+
supportedScopes: string[];
|
|
157
163
|
resources?: string[];
|
|
158
164
|
mcpResources?: {
|
|
159
165
|
url: string;
|
|
@@ -177,7 +183,6 @@ export interface ReferenceIssuerOptions {
|
|
|
177
183
|
device?: {
|
|
178
184
|
endpoint: string;
|
|
179
185
|
verificationUri: string;
|
|
180
|
-
scopes: string[];
|
|
181
186
|
intervalSeconds?: number;
|
|
182
187
|
};
|
|
183
188
|
mediated?: MediatedOptions;
|
|
@@ -196,7 +201,6 @@ export type AuthorizationDecision = {
|
|
|
196
201
|
denied: true;
|
|
197
202
|
};
|
|
198
203
|
export interface ReferenceIssuer {
|
|
199
|
-
browserSessions: BrowserSessionAuthority;
|
|
200
204
|
token(request: Request): Promise<Response>;
|
|
201
205
|
revoke(request: Request): Promise<Response>;
|
|
202
206
|
beginAuthorization(request: Request, browserBinding: string): Promise<AuthorizationView>;
|
|
@@ -204,7 +208,6 @@ export interface ReferenceIssuer {
|
|
|
204
208
|
deviceAuthorization(request: Request): Promise<Response>;
|
|
205
209
|
beginDeviceAuthorization(userCode: string, browserBinding: string): Promise<AuthorizationView>;
|
|
206
210
|
completeDeviceAuthorization(userCode: string, browserBinding: string, decision: AuthorizationDecision): Promise<void>;
|
|
207
|
-
mediatedSignIn(request: Request): Promise<Response>;
|
|
208
211
|
authorizationMetadata(): Record<string, unknown>;
|
|
209
212
|
authority: ExternalAuthorityAdapter;
|
|
210
213
|
disconnect(clientId: string, accountId: string): Promise<void>;
|
|
@@ -214,5 +217,7 @@ export declare class AuthorizationError extends Error {
|
|
|
214
217
|
readonly redirectTo?: URL | undefined;
|
|
215
218
|
constructor(code: string, redirectTo?: URL | undefined);
|
|
216
219
|
}
|
|
220
|
+
/** Run once over the complete namespace, under the adapter's writer lock. */
|
|
221
|
+
export declare function upgradeIssuerState(state: IssuerState): void;
|
|
217
222
|
export declare function initialIssuerState(): IssuerState;
|
|
218
223
|
export declare function createReferenceIssuer(options: ReferenceIssuerOptions): ReferenceIssuer;
|