poe-code 4.0.26 → 4.0.27-beta.1
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/toolcraft/dist/http-hosted-oauth.compile-check.js +6 -0
- package/packages/toolcraft/dist/http-hosted-oauth.d.ts +3 -1
- package/packages/toolcraft/dist/http-hosted-oauth.js +5 -1
- package/packages/toolcraft/dist/testing/hosted-oauth-storage.d.ts +6 -0
- package/packages/toolcraft/dist/testing/hosted-oauth-storage.js +140 -0
- package/packages/toolcraft/dist/testing/index.d.ts +1 -0
- package/packages/toolcraft/dist/testing/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { hostedOAuth } from "./http-hosted-oauth.js";
|
|
2
|
+
const ignoredRevocationObserver = async (grant) => {
|
|
3
|
+
const ignoredSubject = grant.subject;
|
|
4
|
+
void ignoredSubject;
|
|
5
|
+
};
|
|
6
|
+
void ignoredRevocationObserver;
|
|
2
7
|
const ignoredHostedOAuth = hostedOAuth({
|
|
3
8
|
publicUrl: "https://calendar.example/mcp",
|
|
4
9
|
storage: ignoredStorage,
|
|
@@ -30,6 +35,7 @@ const ignoredHostedOAuth = hostedOAuth({
|
|
|
30
35
|
}
|
|
31
36
|
});
|
|
32
37
|
void ignoredHostedOAuth;
|
|
38
|
+
void ignoredHostedOAuth.assertProductionReady();
|
|
33
39
|
const ignoredRedirectHostedOAuth = hostedOAuth({
|
|
34
40
|
publicUrl: "https://calendar.example/mcp",
|
|
35
41
|
storage: ignoredStorage,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JWK } from "jose";
|
|
2
|
-
import { type AuthorizationServerStore, type AuthorizationTransactionRecord, type OAuthAuthorizationServerSigningKey } from "../../mcp-oauth-server/dist/index.js";
|
|
2
|
+
import { type AuthorizationServerStore, type AuthorizationGrantRecord, type AuthorizationTransactionRecord, type OAuthAuthorizationServerSigningKey } from "../../mcp-oauth-server/dist/index.js";
|
|
3
3
|
import type { HttpAdditionalRequestHandler, TinyHttpMcpServerOAuthOptions } from "../../tiny-http-mcp-server/dist/server.js";
|
|
4
4
|
export type HostedOAuthLoginFieldName = "email" | "password" | "apiKey" | (string & {});
|
|
5
5
|
export interface HostedOAuthLoginField {
|
|
@@ -33,6 +33,7 @@ export interface HostedOAuthStorage<TCredential = unknown> {
|
|
|
33
33
|
resolveSubject(providerName: string, accountId: string): Promise<string>;
|
|
34
34
|
healthCheck?(): Promise<void>;
|
|
35
35
|
cleanup?(now?: number): Promise<void>;
|
|
36
|
+
onGrantRevoked?(grant: AuthorizationGrantRecord): Promise<void> | void;
|
|
36
37
|
}
|
|
37
38
|
export interface HostedOAuthCredentialAccess<TCredential = unknown> {
|
|
38
39
|
read(): Promise<TCredential>;
|
|
@@ -105,6 +106,7 @@ export interface HostedOAuthConfiguration<TCredential = unknown, TServices exten
|
|
|
105
106
|
prepare(options?: {
|
|
106
107
|
production?: boolean;
|
|
107
108
|
}): Promise<PreparedHostedOAuth>;
|
|
109
|
+
assertProductionReady(): Promise<PreparedHostedOAuth>;
|
|
108
110
|
}
|
|
109
111
|
export declare function isHostedOAuthConfiguration(value: unknown): value is HostedOAuthConfiguration<unknown, object>;
|
|
110
112
|
export declare function hostedOAuth<TCredential = unknown, TServices extends object = object>(options: HostedOAuthOptions<TCredential, TServices>): HostedOAuthConfiguration<TCredential, TServices>;
|
|
@@ -85,6 +85,9 @@ export function hostedOAuth(options) {
|
|
|
85
85
|
issuer: new URL(publicUrl.origin),
|
|
86
86
|
scopes: this.advanced?.scopes ?? ["mcp", "offline_access"]
|
|
87
87
|
};
|
|
88
|
+
},
|
|
89
|
+
async assertProductionReady() {
|
|
90
|
+
return this.prepare({ production: true });
|
|
88
91
|
}
|
|
89
92
|
};
|
|
90
93
|
}
|
|
@@ -225,7 +228,8 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
225
228
|
accessTokenTtlSeconds: config.advanced?.accessTokenTtlSeconds,
|
|
226
229
|
authorizationCodeTtlSeconds: config.advanced?.authorizationCodeTtlSeconds,
|
|
227
230
|
authorizationTransactionTtlSeconds: config.advanced?.authorizationTransactionTtlSeconds,
|
|
228
|
-
refreshTokenTtlSeconds: config.advanced?.refreshTokenTtlSeconds
|
|
231
|
+
refreshTokenTtlSeconds: config.advanced?.refreshTokenTtlSeconds,
|
|
232
|
+
onGrantRevoked: config.storage.onGrantRevoked
|
|
229
233
|
});
|
|
230
234
|
const requestHandler = async (request, response) => {
|
|
231
235
|
const url = new URL(request.url ?? "/", prepared.issuer);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HostedOAuthStorage } from "../http-hosted-oauth.js";
|
|
2
|
+
export interface HostedOAuthStorageConformanceOptions<TCredential> {
|
|
3
|
+
createStorage(): HostedOAuthStorage<TCredential> | Promise<HostedOAuthStorage<TCredential>>;
|
|
4
|
+
credentials: readonly [TCredential, TCredential];
|
|
5
|
+
}
|
|
6
|
+
export declare function verifyHostedOAuthStorage<TCredential>(options: HostedOAuthStorageConformanceOptions<TCredential>): Promise<void>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
function assert(condition, message) {
|
|
3
|
+
if (!condition)
|
|
4
|
+
throw new Error(`Hosted OAuth storage conformance failed: ${message}`);
|
|
5
|
+
}
|
|
6
|
+
export async function verifyHostedOAuthStorage(options) {
|
|
7
|
+
const storage = await options.createStorage();
|
|
8
|
+
const [firstCredential, secondCredential] = options.credentials;
|
|
9
|
+
const firstSubject = await storage.resolveSubject("provider", "account-a");
|
|
10
|
+
const repeatedSubject = await storage.resolveSubject("provider", "account-a");
|
|
11
|
+
const secondSubject = await storage.resolveSubject("provider", "account-b");
|
|
12
|
+
const otherProviderSubject = await storage.resolveSubject("other-provider", "account-a");
|
|
13
|
+
assert(firstSubject === repeatedSubject, "subject resolution must be stable");
|
|
14
|
+
assert(firstSubject !== secondSubject, "different provider accounts must resolve independently");
|
|
15
|
+
assert(firstSubject !== otherProviderSubject, "provider namespaces must resolve independently");
|
|
16
|
+
await storage.credentials.set(firstSubject, firstCredential);
|
|
17
|
+
assert(isDeepStrictEqual(await storage.credentials.get(firstSubject), firstCredential), "credentials must round-trip");
|
|
18
|
+
await storage.credentials.update(firstSubject, () => secondCredential);
|
|
19
|
+
assert(isDeepStrictEqual(await storage.credentials.get(firstSubject), secondCredential), "credential updates must persist");
|
|
20
|
+
let releaseFirstUpdate;
|
|
21
|
+
let markFirstUpdateStarted;
|
|
22
|
+
const firstUpdateStarted = new Promise((resolve) => {
|
|
23
|
+
markFirstUpdateStarted = resolve;
|
|
24
|
+
});
|
|
25
|
+
const releaseFirst = new Promise((resolve) => {
|
|
26
|
+
releaseFirstUpdate = resolve;
|
|
27
|
+
});
|
|
28
|
+
const firstUpdate = storage.credentials.update(firstSubject, async (current) => {
|
|
29
|
+
assert(isDeepStrictEqual(current, secondCredential), "credential updates must read current data");
|
|
30
|
+
markFirstUpdateStarted?.();
|
|
31
|
+
await releaseFirst;
|
|
32
|
+
return firstCredential;
|
|
33
|
+
});
|
|
34
|
+
await firstUpdateStarted;
|
|
35
|
+
let secondUpdateStarted = false;
|
|
36
|
+
const secondUpdate = storage.credentials.update(firstSubject, (current) => {
|
|
37
|
+
secondUpdateStarted = true;
|
|
38
|
+
assert(isDeepStrictEqual(current, firstCredential), "credential updates must execute atomically");
|
|
39
|
+
return secondCredential;
|
|
40
|
+
});
|
|
41
|
+
const updates = Promise.all([firstUpdate, secondUpdate]);
|
|
42
|
+
await Promise.resolve();
|
|
43
|
+
assert(!secondUpdateStarted, "credential updates must be serialized");
|
|
44
|
+
releaseFirstUpdate?.();
|
|
45
|
+
await updates;
|
|
46
|
+
assert(isDeepStrictEqual(await storage.credentials.get(firstSubject), secondCredential), "serialized credential updates must persist");
|
|
47
|
+
await storage.credentials.delete(firstSubject);
|
|
48
|
+
assert((await storage.credentials.get(firstSubject)) === undefined, "credential deletion must persist");
|
|
49
|
+
const transaction = {
|
|
50
|
+
id: "interaction-1",
|
|
51
|
+
clientId: "client-1",
|
|
52
|
+
redirectUri: "https://client.example/callback",
|
|
53
|
+
codeChallenge: "challenge",
|
|
54
|
+
resource: "https://resource.example/mcp",
|
|
55
|
+
scopes: ["mcp"],
|
|
56
|
+
createdAt: 1,
|
|
57
|
+
expiresAt: 2
|
|
58
|
+
};
|
|
59
|
+
await storage.interactions.set(transaction);
|
|
60
|
+
assert(isDeepStrictEqual(await storage.interactions.get(transaction.id), transaction), "interactions must round-trip");
|
|
61
|
+
await storage.interactions.delete(transaction.id);
|
|
62
|
+
assert((await storage.interactions.get(transaction.id)) === undefined, "interaction deletion must persist");
|
|
63
|
+
const firstKey = await storage.signingKey();
|
|
64
|
+
const secondKey = await storage.signingKey();
|
|
65
|
+
assert(firstKey.keyId === secondKey.keyId && isDeepStrictEqual(firstKey.publicJwk, secondKey.publicJwk), "signing keys must remain stable");
|
|
66
|
+
const restartedStorage = await options.createStorage();
|
|
67
|
+
if (storage.capabilities.stableKeys) {
|
|
68
|
+
const restartedKey = await restartedStorage.signingKey();
|
|
69
|
+
assert(firstKey.keyId === restartedKey.keyId &&
|
|
70
|
+
isDeepStrictEqual(firstKey.publicJwk, restartedKey.publicJwk), "stable signing keys must survive adapter restart");
|
|
71
|
+
}
|
|
72
|
+
if (storage.capabilities.durable) {
|
|
73
|
+
assert((await restartedStorage.resolveSubject("provider", "account-a")) === firstSubject, "durable subject resolution must survive adapter restart");
|
|
74
|
+
}
|
|
75
|
+
const store = storage.authorizationServer;
|
|
76
|
+
const client = { id: "client-1", redirectUris: ["https://client.example/callback"], createdAt: 1 };
|
|
77
|
+
await store.putClient(client);
|
|
78
|
+
assert(isDeepStrictEqual(await store.getClient(client.id), client), "clients must round-trip");
|
|
79
|
+
await store.putAuthorizationTransaction(transaction);
|
|
80
|
+
assert(isDeepStrictEqual(await store.takeAuthorizationTransaction(transaction.id), transaction) &&
|
|
81
|
+
(await store.takeAuthorizationTransaction(transaction.id)) === undefined, "authorization transactions must be consumed atomically");
|
|
82
|
+
const grant = {
|
|
83
|
+
id: "grant-1",
|
|
84
|
+
clientId: client.id,
|
|
85
|
+
subject: firstSubject,
|
|
86
|
+
resource: transaction.resource,
|
|
87
|
+
scopes: ["mcp", "offline_access"],
|
|
88
|
+
createdAt: 1
|
|
89
|
+
};
|
|
90
|
+
const code = {
|
|
91
|
+
tokenHash: "code-1",
|
|
92
|
+
grantId: grant.id,
|
|
93
|
+
clientId: client.id,
|
|
94
|
+
subject: firstSubject,
|
|
95
|
+
redirectUri: transaction.redirectUri,
|
|
96
|
+
codeChallenge: transaction.codeChallenge,
|
|
97
|
+
resource: transaction.resource,
|
|
98
|
+
scopes: grant.scopes,
|
|
99
|
+
expiresAt: 10
|
|
100
|
+
};
|
|
101
|
+
await store.putAuthorizationCode(code);
|
|
102
|
+
assert(isDeepStrictEqual(await store.takeAuthorizationCode(code.tokenHash), code) &&
|
|
103
|
+
(await store.takeAuthorizationCode(code.tokenHash)) === undefined, "authorization codes must be consumed atomically");
|
|
104
|
+
await store.putGrant(grant);
|
|
105
|
+
assert(isDeepStrictEqual(await store.getGrant(grant.id), grant), "grants must round-trip");
|
|
106
|
+
const accessToken = {
|
|
107
|
+
tokenHash: "access-1",
|
|
108
|
+
tokenId: "token-1",
|
|
109
|
+
grantId: grant.id,
|
|
110
|
+
subject: firstSubject,
|
|
111
|
+
clientId: client.id,
|
|
112
|
+
resource: transaction.resource,
|
|
113
|
+
expiresAt: 10
|
|
114
|
+
};
|
|
115
|
+
await store.putAccessToken(accessToken);
|
|
116
|
+
assert(isDeepStrictEqual(await store.getAccessToken(accessToken.tokenHash), accessToken), "access tokens must round-trip");
|
|
117
|
+
await store.revokeToken(accessToken.tokenHash, 5);
|
|
118
|
+
assert((await store.getAccessToken(accessToken.tokenHash))?.revokedAt === 5, "access-token revocation must persist");
|
|
119
|
+
const refreshToken = {
|
|
120
|
+
tokenHash: "refresh-1",
|
|
121
|
+
familyId: "family-1",
|
|
122
|
+
grantId: grant.id,
|
|
123
|
+
clientId: client.id,
|
|
124
|
+
subject: firstSubject,
|
|
125
|
+
resource: transaction.resource,
|
|
126
|
+
scopes: grant.scopes,
|
|
127
|
+
createdAt: 1,
|
|
128
|
+
expiresAt: 10,
|
|
129
|
+
status: "active"
|
|
130
|
+
};
|
|
131
|
+
await store.putRefreshToken(refreshToken);
|
|
132
|
+
const rotation = await store.rotateRefreshToken(refreshToken.tokenHash, "refresh-2", 2, 20);
|
|
133
|
+
assert(rotation.status === "rotated" && isDeepStrictEqual(rotation.previous, refreshToken), "refresh tokens must rotate atomically");
|
|
134
|
+
assert((await store.rotateRefreshToken(refreshToken.tokenHash, "refresh-3", 3, 20)).status ===
|
|
135
|
+
"replay", "refresh-token replay must be detected");
|
|
136
|
+
await store.revokeGrant(grant.id, 6);
|
|
137
|
+
assert((await store.getGrant(grant.id))?.revokedAt === 6, "grant revocation must persist");
|
|
138
|
+
await storage.healthCheck?.();
|
|
139
|
+
await storage.cleanup?.(Date.now());
|
|
140
|
+
}
|
|
@@ -2,3 +2,4 @@ export { createCommandTestHarness, type CommandTestHarness, type ConfirmationReq
|
|
|
2
2
|
export { fakeFetch, fakeService, type FetchRoute, type ServiceCall } from "./fakes.js";
|
|
3
3
|
export { createMemoryFs, type FsChange, type MemoryFs } from "./memory-fs.js";
|
|
4
4
|
export type { ParityResult, SurfaceOutcome } from "./parity.js";
|
|
5
|
+
export { verifyHostedOAuthStorage, type HostedOAuthStorageConformanceOptions } from "./hosted-oauth-storage.js";
|