@treeseed/core 0.4.1 → 0.4.4
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/README.md +7 -1
- package/dist/api/agent-routes.d.ts +13 -0
- package/dist/api/agent-routes.js +402 -0
- package/dist/api/app.d.ts +5 -0
- package/dist/api/app.js +270 -0
- package/dist/api/auth/d1-database.d.ts +3 -0
- package/dist/api/auth/d1-database.js +24 -0
- package/dist/api/auth/d1-provider.d.ts +67 -0
- package/dist/api/auth/d1-provider.js +84 -0
- package/dist/api/auth/d1-store.d.ts +97 -0
- package/dist/api/auth/d1-store.js +631 -0
- package/dist/api/auth/memory-provider.d.ts +73 -0
- package/dist/api/auth/memory-provider.js +239 -0
- package/dist/api/auth/rbac.d.ts +22 -0
- package/dist/api/auth/rbac.js +158 -0
- package/dist/api/auth/tokens.d.ts +18 -0
- package/dist/api/auth/tokens.js +56 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +65 -0
- package/dist/api/gateway.d.ts +5 -0
- package/dist/api/gateway.js +35 -0
- package/dist/api/http.d.ts +24 -0
- package/dist/api/http.js +44 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +18 -0
- package/dist/api/operations-routes.d.ts +6 -0
- package/dist/api/operations-routes.js +34 -0
- package/dist/api/operations.d.ts +3 -0
- package/dist/api/operations.js +26 -0
- package/dist/api/providers.d.ts +2 -0
- package/dist/api/providers.js +61 -0
- package/dist/api/railway.d.ts +45 -0
- package/dist/api/railway.js +69 -0
- package/dist/api/sdk-dispatch.d.ts +14 -0
- package/dist/api/sdk-dispatch.js +145 -0
- package/dist/api/sdk-routes.d.ts +10 -0
- package/dist/api/sdk-routes.js +25 -0
- package/dist/api/server.d.ts +2 -0
- package/dist/api/server.js +10 -0
- package/dist/api/templates.d.ts +3 -0
- package/dist/api/templates.js +31 -0
- package/dist/api/types.d.ts +193 -0
- package/dist/api/types.js +0 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/dev.d.ts +41 -0
- package/dist/dev.js +189 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +23 -1
- package/dist/platform-resources.d.ts +37 -0
- package/dist/platform-resources.js +133 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +16 -0
- package/dist/plugin-default.d.ts +1 -0
- package/dist/plugin-default.js +4 -0
- package/dist/railway.d.ts +1 -0
- package/dist/railway.js +4 -0
- package/dist/scripts/build-dist.js +7 -0
- package/dist/scripts/dev-platform.js +24 -0
- package/dist/scripts/workspace-bootstrap.js +24 -10
- package/dist/site-resources.d.ts +1 -29
- package/dist/site-resources.js +7 -120
- package/dist/site.js +3 -1
- package/package.json +37 -3
package/dist/api/app.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { AgentSdk, TREESEED_REMOTE_CONTRACT_HEADER, TREESEED_REMOTE_CONTRACT_VERSION } from "@treeseed/sdk";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { registerAgentRoutes } from "./agent-routes.js";
|
|
5
|
+
import { resolveApiConfig } from "./config.js";
|
|
6
|
+
import { bearerTokenFromRequest, jsonError, requireAuthentication, requirePermission, requireScope } from "./http.js";
|
|
7
|
+
import { registerOperationRoutes } from "./operations-routes.js";
|
|
8
|
+
import { resolveApiRuntimeProviders } from "./providers.js";
|
|
9
|
+
import { registerSdkRoutes } from "./sdk-routes.js";
|
|
10
|
+
import { loadTemplateCatalog } from "./templates.js";
|
|
11
|
+
function mergeApiOptions(options) {
|
|
12
|
+
const baseConfig = resolveApiConfig();
|
|
13
|
+
return {
|
|
14
|
+
config: {
|
|
15
|
+
...baseConfig,
|
|
16
|
+
...options.config ?? {},
|
|
17
|
+
providers: {
|
|
18
|
+
...baseConfig.providers,
|
|
19
|
+
...options.config?.providers ?? {},
|
|
20
|
+
agents: {
|
|
21
|
+
...baseConfig.providers.agents,
|
|
22
|
+
...options.config?.providers?.agents ?? {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
surfaces: {
|
|
27
|
+
auth: true,
|
|
28
|
+
templates: true,
|
|
29
|
+
sdk: true,
|
|
30
|
+
agent: true,
|
|
31
|
+
operations: true,
|
|
32
|
+
...options.surfaces ?? {}
|
|
33
|
+
},
|
|
34
|
+
scopes: {
|
|
35
|
+
authMe: "auth:me",
|
|
36
|
+
sdk: "sdk",
|
|
37
|
+
agent: "agent",
|
|
38
|
+
operations: "operations",
|
|
39
|
+
...options.scopes ?? {}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function createTreeseedApiApp(options = {}) {
|
|
44
|
+
const resolved = mergeApiOptions(options);
|
|
45
|
+
const runtimeProviders = resolveApiRuntimeProviders(resolved.config, options.runtimeProviders);
|
|
46
|
+
const sharedSdk = options.sdk ?? new AgentSdk({ repoRoot: resolved.config.repoRoot });
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
app.use("*", async (c, next) => {
|
|
49
|
+
c.set("requestId", crypto.randomUUID());
|
|
50
|
+
c.set("config", resolved.config);
|
|
51
|
+
c.set("principal", null);
|
|
52
|
+
c.set("actingUser", null);
|
|
53
|
+
c.set("credential", null);
|
|
54
|
+
c.set("actorType", "anonymous");
|
|
55
|
+
c.set("permissionGrants", []);
|
|
56
|
+
c.header(TREESEED_REMOTE_CONTRACT_HEADER, String(TREESEED_REMOTE_CONTRACT_VERSION));
|
|
57
|
+
await next();
|
|
58
|
+
});
|
|
59
|
+
app.use("*", async (c, next) => {
|
|
60
|
+
const serviceId = c.req.header("x-treeseed-service-id");
|
|
61
|
+
const serviceSecret = c.req.header("x-treeseed-service-secret");
|
|
62
|
+
if (serviceId && serviceSecret) {
|
|
63
|
+
const result = await runtimeProviders.auth.authenticateServiceCredential(serviceId, serviceSecret);
|
|
64
|
+
if (!result) {
|
|
65
|
+
return jsonError(c, 401, "Invalid internal service credential.");
|
|
66
|
+
}
|
|
67
|
+
c.set("principal", result.principal);
|
|
68
|
+
c.set("credential", result.credential);
|
|
69
|
+
c.set("actorType", "service");
|
|
70
|
+
c.set("permissionGrants", result.principal.permissions);
|
|
71
|
+
}
|
|
72
|
+
await next();
|
|
73
|
+
});
|
|
74
|
+
app.use("*", async (c, next) => {
|
|
75
|
+
const token = bearerTokenFromRequest(c.req.raw);
|
|
76
|
+
if (token) {
|
|
77
|
+
const result = await runtimeProviders.auth.authenticateBearerToken(token);
|
|
78
|
+
if (result) {
|
|
79
|
+
c.set("principal", result.principal);
|
|
80
|
+
c.set("credential", result.credential);
|
|
81
|
+
c.set("actorType", result.credential.type === "service_token" ? "service" : "user");
|
|
82
|
+
c.set("permissionGrants", result.principal.permissions);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await next();
|
|
86
|
+
});
|
|
87
|
+
app.use("*", async (c, next) => {
|
|
88
|
+
const assertion = c.req.header("x-treeseed-user-assertion");
|
|
89
|
+
if (c.get("actorType") === "service" && assertion) {
|
|
90
|
+
const claims = runtimeProviders.auth.verifyTrustedUserAssertion(assertion);
|
|
91
|
+
if (!claims) {
|
|
92
|
+
return jsonError(c, 401, "Invalid trusted user assertion.");
|
|
93
|
+
}
|
|
94
|
+
const exchange = await runtimeProviders.auth.exchangeTrustedUserAssertion(claims);
|
|
95
|
+
c.set("actingUser", exchange.principal);
|
|
96
|
+
c.set("principal", exchange.principal);
|
|
97
|
+
c.set("actorType", "user");
|
|
98
|
+
c.set("permissionGrants", exchange.principal.permissions);
|
|
99
|
+
}
|
|
100
|
+
await next();
|
|
101
|
+
});
|
|
102
|
+
app.get("/healthz", (c) => c.json({
|
|
103
|
+
ok: true,
|
|
104
|
+
service: resolved.config.name,
|
|
105
|
+
status: "ok",
|
|
106
|
+
requestId: c.get("requestId")
|
|
107
|
+
}));
|
|
108
|
+
app.get("/readyz", (c) => c.json({
|
|
109
|
+
ok: true,
|
|
110
|
+
ready: true,
|
|
111
|
+
providers: runtimeProviders.selections,
|
|
112
|
+
surfaces: resolved.surfaces
|
|
113
|
+
}));
|
|
114
|
+
if (resolved.surfaces.templates) {
|
|
115
|
+
app.get("/templates", (c) => c.json(loadTemplateCatalog(resolved.config)));
|
|
116
|
+
app.get("/search/templates", (c) => c.json(loadTemplateCatalog(resolved.config)));
|
|
117
|
+
app.get("/templates/:id", (c) => {
|
|
118
|
+
const catalog = loadTemplateCatalog(resolved.config);
|
|
119
|
+
const item = catalog.items.find((entry) => entry.id === c.req.param("id"));
|
|
120
|
+
return item ? c.json({ ok: true, payload: item }) : jsonError(c, 404, `Unknown template "${c.req.param("id")}".`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (resolved.surfaces.auth) {
|
|
124
|
+
app.post("/auth/device/start", async (c) => {
|
|
125
|
+
const body = await c.req.json().catch(() => ({}));
|
|
126
|
+
return c.json(await runtimeProviders.auth.startDeviceFlow(body));
|
|
127
|
+
});
|
|
128
|
+
app.post("/auth/device/poll", async (c) => {
|
|
129
|
+
const body = await c.req.json().catch(() => ({}));
|
|
130
|
+
const response = await runtimeProviders.auth.pollDeviceFlow(body);
|
|
131
|
+
return c.json(response, { status: response.ok ? 200 : response.status === "expired" ? 410 : 400 });
|
|
132
|
+
});
|
|
133
|
+
app.post("/auth/device/approve", async (c) => {
|
|
134
|
+
const body = await c.req.json().catch(() => ({}));
|
|
135
|
+
try {
|
|
136
|
+
return c.json(await runtimeProviders.auth.approveDeviceFlow(body));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return jsonError(c, 400, error instanceof Error ? error.message : String(error));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
app.post("/auth/token/refresh", async (c) => {
|
|
142
|
+
const body = await c.req.json().catch(() => ({}));
|
|
143
|
+
try {
|
|
144
|
+
return c.json(await runtimeProviders.auth.refreshAccessToken(body));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return jsonError(c, 401, error instanceof Error ? error.message : String(error));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
app.get("/auth/me", (c) => {
|
|
150
|
+
const unauthorized = requireScope(c, resolved.scopes.authMe);
|
|
151
|
+
if (unauthorized) return unauthorized;
|
|
152
|
+
return c.json({
|
|
153
|
+
ok: true,
|
|
154
|
+
payload: c.get("principal")
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
app.post("/auth/pat", async (c) => {
|
|
158
|
+
const unauthorized = requirePermission(c, "api_tokens:create:self");
|
|
159
|
+
if (unauthorized) return unauthorized;
|
|
160
|
+
const principal = c.get("principal");
|
|
161
|
+
const body = await c.req.json().catch(() => ({}));
|
|
162
|
+
if (!body.name?.trim() || !principal) {
|
|
163
|
+
return jsonError(c, 400, "Token name is required.");
|
|
164
|
+
}
|
|
165
|
+
return c.json({
|
|
166
|
+
ok: true,
|
|
167
|
+
payload: await runtimeProviders.auth.createPersonalAccessToken(principal.id, {
|
|
168
|
+
name: body.name.trim(),
|
|
169
|
+
scopes: body.scopes,
|
|
170
|
+
expiresAt: body.expiresAt ?? null
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
app.get("/auth/pat", async (c) => {
|
|
175
|
+
const unauthorized = requirePermission(c, "api_tokens:read:self");
|
|
176
|
+
if (unauthorized) return unauthorized;
|
|
177
|
+
const principal = c.get("principal");
|
|
178
|
+
if (!principal) return jsonError(c, 401, "Authentication required.");
|
|
179
|
+
return c.json({
|
|
180
|
+
ok: true,
|
|
181
|
+
payload: await runtimeProviders.auth.listPersonalAccessTokens(principal.id)
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
app.delete("/auth/pat/:id", async (c) => {
|
|
185
|
+
const unauthorized = requirePermission(c, "api_tokens:delete:self");
|
|
186
|
+
if (unauthorized) return unauthorized;
|
|
187
|
+
const principal = c.get("principal");
|
|
188
|
+
if (!principal) return jsonError(c, 401, "Authentication required.");
|
|
189
|
+
await runtimeProviders.auth.revokePersonalAccessToken(principal.id, c.req.param("id"));
|
|
190
|
+
return c.json({ ok: true });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
app.post("/internal/auth/web/sync-user", async (c) => {
|
|
194
|
+
if (c.get("actorType") !== "service") {
|
|
195
|
+
return jsonError(c, 401, "Trusted service authentication required.");
|
|
196
|
+
}
|
|
197
|
+
const unauthorized = requirePermission(c, "services:impersonate:global");
|
|
198
|
+
if (unauthorized) return unauthorized;
|
|
199
|
+
const body = await c.req.json().catch(() => ({}));
|
|
200
|
+
return c.json({
|
|
201
|
+
ok: true,
|
|
202
|
+
payload: await runtimeProviders.auth.syncUserIdentity(body)
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
app.post("/internal/auth/web/exchange", async (c) => {
|
|
206
|
+
if (c.get("actorType") !== "service") {
|
|
207
|
+
return jsonError(c, 401, "Trusted service authentication required.");
|
|
208
|
+
}
|
|
209
|
+
const unauthorized = requirePermission(c, "services:impersonate:global");
|
|
210
|
+
if (unauthorized) return unauthorized;
|
|
211
|
+
const body = await c.req.json().catch(() => ({}));
|
|
212
|
+
return c.json(await runtimeProviders.auth.exchangeTrustedUserAssertion(body));
|
|
213
|
+
});
|
|
214
|
+
app.post("/internal/auth/service/token", async (c) => {
|
|
215
|
+
const unauthorized = requirePermission(c, "services:manage:global");
|
|
216
|
+
if (unauthorized) return unauthorized;
|
|
217
|
+
const body = await c.req.json().catch(() => ({}));
|
|
218
|
+
if (!body.serviceId?.trim() || !body.name?.trim()) {
|
|
219
|
+
return jsonError(c, 400, "serviceId and name are required.");
|
|
220
|
+
}
|
|
221
|
+
return c.json({
|
|
222
|
+
ok: true,
|
|
223
|
+
payload: await runtimeProviders.auth.createServiceToken({
|
|
224
|
+
serviceId: body.serviceId.trim(),
|
|
225
|
+
name: body.name.trim(),
|
|
226
|
+
roles: body.roles,
|
|
227
|
+
permissions: body.permissions
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
app.post("/internal/auth/service/rotate", async (c) => {
|
|
232
|
+
const unauthorized = requirePermission(c, "services:manage:global");
|
|
233
|
+
if (unauthorized) return unauthorized;
|
|
234
|
+
const body = await c.req.json().catch(() => ({}));
|
|
235
|
+
if (!body.serviceId?.trim()) {
|
|
236
|
+
return jsonError(c, 400, "serviceId is required.");
|
|
237
|
+
}
|
|
238
|
+
return c.json({
|
|
239
|
+
ok: true,
|
|
240
|
+
payload: await runtimeProviders.auth.rotateServiceToken(body.serviceId.trim())
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
if (resolved.surfaces.sdk) {
|
|
244
|
+
registerSdkRoutes(app, {
|
|
245
|
+
config: resolved.config,
|
|
246
|
+
sharedSdk,
|
|
247
|
+
scope: resolved.scopes.sdk
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (resolved.surfaces.agent) {
|
|
251
|
+
registerAgentRoutes(app, {
|
|
252
|
+
sdk: sharedSdk,
|
|
253
|
+
prefix: "/agent",
|
|
254
|
+
scope: resolved.scopes.agent,
|
|
255
|
+
projectId: "treeseed-market",
|
|
256
|
+
defaultActor: "api"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (resolved.surfaces.operations) {
|
|
260
|
+
registerOperationRoutes(app, {
|
|
261
|
+
scope: resolved.scopes.operations,
|
|
262
|
+
executeOperation: options.workflowExecutor
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
app.notFound((c) => jsonError(c, 404, "Not found."));
|
|
266
|
+
return app;
|
|
267
|
+
}
|
|
268
|
+
export {
|
|
269
|
+
createTreeseedApiApp
|
|
270
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CloudflareHttpD1Database } from "@treeseed/sdk";
|
|
2
|
+
import { WranglerD1Database } from "@treeseed/sdk/wrangler-d1";
|
|
3
|
+
function resolveApiD1Database(config) {
|
|
4
|
+
if (config.cloudflareAccountId && config.cloudflareApiToken && config.d1DatabaseId) {
|
|
5
|
+
return new CloudflareHttpD1Database({
|
|
6
|
+
accountId: config.cloudflareAccountId,
|
|
7
|
+
apiToken: config.cloudflareApiToken,
|
|
8
|
+
databaseId: config.d1DatabaseId
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (config.d1DatabaseName) {
|
|
12
|
+
return new WranglerD1Database(
|
|
13
|
+
config.d1DatabaseName,
|
|
14
|
+
config.repoRoot,
|
|
15
|
+
config.d1LocalPersistTo || void 0
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Treeseed API auth requires either CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN + TREESEED_API_D1_DATABASE_ID for remote D1 access, or TREESEED_API_D1_DATABASE_NAME for local Wrangler-backed D1 access."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
resolveApiD1Database
|
|
24
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare';
|
|
2
|
+
import type { ApiAuthProvider, ApiConfig, ApiCredential, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims, UserIdentityProfileInput } from '../types.ts';
|
|
3
|
+
export declare class D1AuthProvider implements ApiAuthProvider {
|
|
4
|
+
private readonly config;
|
|
5
|
+
readonly id = "d1";
|
|
6
|
+
private readonly store;
|
|
7
|
+
constructor(config: ApiConfig, options?: {
|
|
8
|
+
db?: D1DatabaseLike;
|
|
9
|
+
});
|
|
10
|
+
startDeviceFlow(request: DeviceCodeStartRequest): Promise<DeviceCodeStartResponse>;
|
|
11
|
+
pollDeviceFlow(request: DeviceCodePollRequest): Promise<DeviceCodePollResponse>;
|
|
12
|
+
refreshAccessToken(request: TokenRefreshRequest): Promise<TokenRefreshResponse>;
|
|
13
|
+
approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{
|
|
14
|
+
ok: true;
|
|
15
|
+
}>;
|
|
16
|
+
authenticateBearerToken(token: string): Promise<{
|
|
17
|
+
principal: ApiPrincipal;
|
|
18
|
+
credential: ApiCredential;
|
|
19
|
+
} | null>;
|
|
20
|
+
authenticateServiceCredential(serviceId: string, secret: string): Promise<{
|
|
21
|
+
principal: ApiPrincipal;
|
|
22
|
+
credential: ApiCredential;
|
|
23
|
+
} | null>;
|
|
24
|
+
createPersonalAccessToken(userId: string, input: {
|
|
25
|
+
name: string;
|
|
26
|
+
scopes?: string[];
|
|
27
|
+
expiresAt?: string | null;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
30
|
+
token: string;
|
|
31
|
+
prefix: string;
|
|
32
|
+
name: string;
|
|
33
|
+
expiresAt: string;
|
|
34
|
+
}>;
|
|
35
|
+
listPersonalAccessTokens(userId: string): Promise<{
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
token_prefix: string;
|
|
39
|
+
expires_at: string | null;
|
|
40
|
+
last_used_at: string | null;
|
|
41
|
+
revoked_at: string | null;
|
|
42
|
+
created_at: string;
|
|
43
|
+
}[]>;
|
|
44
|
+
revokePersonalAccessToken(userId: string, tokenId: string): Promise<void>;
|
|
45
|
+
syncUserIdentity(identity: UserIdentityProfileInput): Promise<{
|
|
46
|
+
identityId: string;
|
|
47
|
+
principal: ApiPrincipal;
|
|
48
|
+
userId: string;
|
|
49
|
+
}>;
|
|
50
|
+
createServiceToken(input: {
|
|
51
|
+
serviceId: string;
|
|
52
|
+
name: string;
|
|
53
|
+
roles?: string[];
|
|
54
|
+
permissions?: string[];
|
|
55
|
+
}): Promise<import("./d1-store.ts").ServiceCredentialResult>;
|
|
56
|
+
rotateServiceToken(serviceId: string): Promise<import("./d1-store.ts").ServiceCredentialResult>;
|
|
57
|
+
createTrustedUserAssertion(claims: TrustedUserAssertionClaims): string;
|
|
58
|
+
verifyTrustedUserAssertion(assertion: string): TrustedUserAssertionClaims;
|
|
59
|
+
exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{
|
|
60
|
+
ok: true;
|
|
61
|
+
accessToken: string;
|
|
62
|
+
tokenType: "Bearer";
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
expiresInSeconds: number;
|
|
65
|
+
principal: ApiPrincipal;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { resolveApiD1Database } from "./d1-database.js";
|
|
3
|
+
import { D1AuthStore } from "./d1-store.js";
|
|
4
|
+
function encodePayload(payload) {
|
|
5
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
6
|
+
}
|
|
7
|
+
function decodePayload(value) {
|
|
8
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
9
|
+
}
|
|
10
|
+
function signPayload(payload, secret) {
|
|
11
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
12
|
+
}
|
|
13
|
+
function safeEqual(left, right) {
|
|
14
|
+
const leftBuffer = Buffer.from(left);
|
|
15
|
+
const rightBuffer = Buffer.from(right);
|
|
16
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
17
|
+
}
|
|
18
|
+
class D1AuthProvider {
|
|
19
|
+
constructor(config, options = {}) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.store = new D1AuthStore(config, options.db ?? resolveApiD1Database(config));
|
|
22
|
+
}
|
|
23
|
+
config;
|
|
24
|
+
id = "d1";
|
|
25
|
+
store;
|
|
26
|
+
startDeviceFlow(request) {
|
|
27
|
+
return this.store.startDeviceFlow(request);
|
|
28
|
+
}
|
|
29
|
+
pollDeviceFlow(request) {
|
|
30
|
+
return this.store.pollDeviceFlow(request);
|
|
31
|
+
}
|
|
32
|
+
refreshAccessToken(request) {
|
|
33
|
+
return this.store.refreshAccessToken(request);
|
|
34
|
+
}
|
|
35
|
+
approveDeviceFlow(request) {
|
|
36
|
+
return this.store.approveDeviceFlow(request);
|
|
37
|
+
}
|
|
38
|
+
authenticateBearerToken(token) {
|
|
39
|
+
return this.store.authenticateBearerToken(token);
|
|
40
|
+
}
|
|
41
|
+
authenticateServiceCredential(serviceId, secret) {
|
|
42
|
+
return this.store.authenticateService(serviceId, secret);
|
|
43
|
+
}
|
|
44
|
+
createPersonalAccessToken(userId, input) {
|
|
45
|
+
return this.store.createPersonalAccessToken(userId, input);
|
|
46
|
+
}
|
|
47
|
+
listPersonalAccessTokens(userId) {
|
|
48
|
+
return this.store.listPersonalAccessTokens(userId);
|
|
49
|
+
}
|
|
50
|
+
revokePersonalAccessToken(userId, tokenId) {
|
|
51
|
+
return this.store.revokePersonalAccessToken(userId, tokenId);
|
|
52
|
+
}
|
|
53
|
+
syncUserIdentity(identity) {
|
|
54
|
+
return this.store.syncUser(identity);
|
|
55
|
+
}
|
|
56
|
+
createServiceToken(input) {
|
|
57
|
+
return this.store.createServiceCredential(input);
|
|
58
|
+
}
|
|
59
|
+
rotateServiceToken(serviceId) {
|
|
60
|
+
return this.store.rotateServiceCredential(serviceId);
|
|
61
|
+
}
|
|
62
|
+
createTrustedUserAssertion(claims) {
|
|
63
|
+
const payload = encodePayload(claims);
|
|
64
|
+
const signature = signPayload(payload, this.config.webAssertionSecret);
|
|
65
|
+
return `${payload}.${signature}`;
|
|
66
|
+
}
|
|
67
|
+
verifyTrustedUserAssertion(assertion) {
|
|
68
|
+
const [payload, signature] = assertion.split(".");
|
|
69
|
+
if (!payload || !signature) return null;
|
|
70
|
+
const expectedSignature = signPayload(payload, this.config.webAssertionSecret);
|
|
71
|
+
if (!safeEqual(signature, expectedSignature)) return null;
|
|
72
|
+
const claims = decodePayload(payload);
|
|
73
|
+
if (!claims.expiresAt || new Date(claims.expiresAt).getTime() <= Date.now()) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return claims;
|
|
77
|
+
}
|
|
78
|
+
exchangeTrustedUserAssertion(claims) {
|
|
79
|
+
return this.store.exchangeTrustedUserAssertion(claims);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
D1AuthProvider
|
|
84
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare';
|
|
2
|
+
import type { ApiConfig, ApiCredential, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims, UserIdentityProfileInput } from '../types.ts';
|
|
3
|
+
export interface PersonalAccessTokenResult {
|
|
4
|
+
id: string;
|
|
5
|
+
token: string;
|
|
6
|
+
prefix: string;
|
|
7
|
+
name: string;
|
|
8
|
+
expiresAt: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ServiceCredentialResult {
|
|
11
|
+
id: string;
|
|
12
|
+
serviceId: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class D1AuthStore {
|
|
16
|
+
private readonly config;
|
|
17
|
+
private readonly db;
|
|
18
|
+
private initializationPromise;
|
|
19
|
+
constructor(config: ApiConfig, db: D1DatabaseLike);
|
|
20
|
+
private run;
|
|
21
|
+
private first;
|
|
22
|
+
private all;
|
|
23
|
+
private ensureInitialized;
|
|
24
|
+
private seedCatalog;
|
|
25
|
+
private seedConfiguredServices;
|
|
26
|
+
private loadUser;
|
|
27
|
+
private loadIdentityByProvider;
|
|
28
|
+
private rolesForUser;
|
|
29
|
+
private permissionsForUser;
|
|
30
|
+
private scopesForPrincipal;
|
|
31
|
+
private principalForUser;
|
|
32
|
+
private assignRole;
|
|
33
|
+
private bootstrapRolesForUser;
|
|
34
|
+
private writeAuditEvent;
|
|
35
|
+
syncUser(identity: UserIdentityProfileInput): Promise<{
|
|
36
|
+
identityId: string;
|
|
37
|
+
principal: ApiPrincipal;
|
|
38
|
+
userId: string;
|
|
39
|
+
}>;
|
|
40
|
+
startDeviceFlow(request: DeviceCodeStartRequest): Promise<DeviceCodeStartResponse>;
|
|
41
|
+
approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{
|
|
42
|
+
ok: true;
|
|
43
|
+
}>;
|
|
44
|
+
pollDeviceFlow(request: DeviceCodePollRequest): Promise<DeviceCodePollResponse>;
|
|
45
|
+
refreshAccessToken(request: TokenRefreshRequest): Promise<TokenRefreshResponse>;
|
|
46
|
+
createPersonalAccessToken(userId: string, input: {
|
|
47
|
+
name: string;
|
|
48
|
+
scopes?: string[];
|
|
49
|
+
expiresAt?: string | null;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
52
|
+
token: string;
|
|
53
|
+
prefix: string;
|
|
54
|
+
name: string;
|
|
55
|
+
expiresAt: string;
|
|
56
|
+
}>;
|
|
57
|
+
listPersonalAccessTokens(userId: string): Promise<{
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
token_prefix: string;
|
|
61
|
+
expires_at: string | null;
|
|
62
|
+
last_used_at: string | null;
|
|
63
|
+
revoked_at: string | null;
|
|
64
|
+
created_at: string;
|
|
65
|
+
}[]>;
|
|
66
|
+
revokePersonalAccessToken(userId: string, tokenId: string): Promise<void>;
|
|
67
|
+
upsertServiceCredential(input: {
|
|
68
|
+
serviceId: string;
|
|
69
|
+
name: string;
|
|
70
|
+
secret: string;
|
|
71
|
+
roles?: string[];
|
|
72
|
+
permissions?: string[];
|
|
73
|
+
}): Promise<string>;
|
|
74
|
+
createServiceCredential(input: {
|
|
75
|
+
serviceId: string;
|
|
76
|
+
name: string;
|
|
77
|
+
roles?: string[];
|
|
78
|
+
permissions?: string[];
|
|
79
|
+
}): Promise<ServiceCredentialResult>;
|
|
80
|
+
rotateServiceCredential(serviceId: string): Promise<ServiceCredentialResult>;
|
|
81
|
+
authenticateBearerToken(token: string): Promise<{
|
|
82
|
+
principal: ApiPrincipal;
|
|
83
|
+
credential: ApiCredential;
|
|
84
|
+
} | null>;
|
|
85
|
+
authenticateService(serviceId: string, secret: string): Promise<{
|
|
86
|
+
principal: ApiPrincipal;
|
|
87
|
+
credential: ApiCredential;
|
|
88
|
+
} | null>;
|
|
89
|
+
exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{
|
|
90
|
+
ok: true;
|
|
91
|
+
accessToken: string;
|
|
92
|
+
tokenType: "Bearer";
|
|
93
|
+
expiresAt: string;
|
|
94
|
+
expiresInSeconds: number;
|
|
95
|
+
principal: ApiPrincipal;
|
|
96
|
+
}>;
|
|
97
|
+
}
|