@treeseed/core 0.4.2 → 0.4.5
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/patch-starlight-content-path.js +1 -1
- package/dist/scripts/workspace-bootstrap.js +16 -3
- 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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
createAccessToken,
|
|
4
|
+
nextOpaqueToken,
|
|
5
|
+
principalFromAccessTokenPayload,
|
|
6
|
+
verifyAccessToken
|
|
7
|
+
} from "./tokens.js";
|
|
8
|
+
function nowSeconds() {
|
|
9
|
+
return Math.floor(Date.now() / 1e3);
|
|
10
|
+
}
|
|
11
|
+
function formatExpiry(epochSeconds) {
|
|
12
|
+
return new Date(epochSeconds * 1e3).toISOString();
|
|
13
|
+
}
|
|
14
|
+
function nextUserCode() {
|
|
15
|
+
return Math.random().toString(36).slice(2, 6).toUpperCase() + "-" + Math.random().toString(36).slice(2, 6).toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
class MemoryDeviceCodeAuthProvider {
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
config;
|
|
22
|
+
id = "memory";
|
|
23
|
+
devices = /* @__PURE__ */ new Map();
|
|
24
|
+
refreshSessions = /* @__PURE__ */ new Map();
|
|
25
|
+
async startDeviceFlow(request) {
|
|
26
|
+
const expiresAt = nowSeconds() + this.config.deviceCodeTtlSeconds;
|
|
27
|
+
const deviceCode = nextOpaqueToken("device");
|
|
28
|
+
const userCode = nextUserCode();
|
|
29
|
+
this.devices.set(deviceCode, {
|
|
30
|
+
deviceCode,
|
|
31
|
+
userCode,
|
|
32
|
+
requestedScopes: request.scopes?.length ? [...request.scopes] : ["templates:read", "auth:me", "sdk", "operations"],
|
|
33
|
+
expiresAt,
|
|
34
|
+
intervalSeconds: this.config.deviceCodePollIntervalSeconds,
|
|
35
|
+
status: "pending",
|
|
36
|
+
principal: null
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
deviceCode,
|
|
41
|
+
userCode,
|
|
42
|
+
verificationUri: `${this.config.baseUrl}/auth/device/approve`,
|
|
43
|
+
verificationUriComplete: `${this.config.baseUrl}/auth/device/approve?user_code=${encodeURIComponent(userCode)}`,
|
|
44
|
+
intervalSeconds: this.config.deviceCodePollIntervalSeconds,
|
|
45
|
+
expiresAt: formatExpiry(expiresAt),
|
|
46
|
+
expiresInSeconds: this.config.deviceCodeTtlSeconds
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async approveDeviceFlow(request) {
|
|
50
|
+
const record = [...this.devices.values()].find((entry) => entry.userCode === request.userCode);
|
|
51
|
+
if (!record || record.expiresAt <= nowSeconds()) {
|
|
52
|
+
throw new Error("Device code approval failed because the user code is unknown or expired.");
|
|
53
|
+
}
|
|
54
|
+
record.status = "approved";
|
|
55
|
+
record.principal = {
|
|
56
|
+
id: request.principalId,
|
|
57
|
+
displayName: request.displayName,
|
|
58
|
+
scopes: request.scopes?.length ? [...request.scopes] : [...record.requestedScopes],
|
|
59
|
+
roles: ["member"],
|
|
60
|
+
permissions: ["auth:read:self"],
|
|
61
|
+
metadata: request.metadata
|
|
62
|
+
};
|
|
63
|
+
return { ok: true };
|
|
64
|
+
}
|
|
65
|
+
async pollDeviceFlow(request) {
|
|
66
|
+
const record = this.devices.get(request.deviceCode);
|
|
67
|
+
if (!record) {
|
|
68
|
+
return { ok: false, status: "invalid", error: "Unknown device code." };
|
|
69
|
+
}
|
|
70
|
+
if (record.expiresAt <= nowSeconds()) {
|
|
71
|
+
this.devices.delete(request.deviceCode);
|
|
72
|
+
return { ok: false, status: "expired", error: "Device code expired." };
|
|
73
|
+
}
|
|
74
|
+
if (record.status === "pending" || !record.principal) {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
status: "pending",
|
|
78
|
+
intervalSeconds: record.intervalSeconds
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (record.status === "used") {
|
|
82
|
+
return { ok: false, status: "already_used", error: "Device code already used." };
|
|
83
|
+
}
|
|
84
|
+
record.status = "used";
|
|
85
|
+
const refreshToken = nextOpaqueToken("refresh");
|
|
86
|
+
const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds;
|
|
87
|
+
const accessToken = createAccessToken({
|
|
88
|
+
sub: record.principal.id,
|
|
89
|
+
displayName: record.principal.displayName,
|
|
90
|
+
scopes: record.principal.scopes,
|
|
91
|
+
roles: record.principal.roles,
|
|
92
|
+
permissions: record.principal.permissions,
|
|
93
|
+
metadata: record.principal.metadata,
|
|
94
|
+
iat: nowSeconds(),
|
|
95
|
+
exp: expiresAt,
|
|
96
|
+
iss: this.config.issuer,
|
|
97
|
+
jti: randomUUID(),
|
|
98
|
+
tokenType: "access"
|
|
99
|
+
}, this.config.authSecret);
|
|
100
|
+
this.refreshSessions.set(refreshToken, {
|
|
101
|
+
principal: record.principal,
|
|
102
|
+
expiresAt: nowSeconds() + this.config.refreshTokenTtlSeconds
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
status: "approved",
|
|
107
|
+
accessToken,
|
|
108
|
+
refreshToken,
|
|
109
|
+
tokenType: "Bearer",
|
|
110
|
+
expiresAt: formatExpiry(expiresAt),
|
|
111
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
112
|
+
principal: record.principal
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async refreshAccessToken(request) {
|
|
116
|
+
const session = this.refreshSessions.get(request.refreshToken);
|
|
117
|
+
if (!session || session.expiresAt <= nowSeconds()) {
|
|
118
|
+
throw new Error("Refresh token is invalid or expired.");
|
|
119
|
+
}
|
|
120
|
+
const nextRefreshToken = nextOpaqueToken("refresh");
|
|
121
|
+
this.refreshSessions.delete(request.refreshToken);
|
|
122
|
+
this.refreshSessions.set(nextRefreshToken, {
|
|
123
|
+
principal: session.principal,
|
|
124
|
+
expiresAt: nowSeconds() + this.config.refreshTokenTtlSeconds
|
|
125
|
+
});
|
|
126
|
+
const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds;
|
|
127
|
+
const accessToken = createAccessToken({
|
|
128
|
+
sub: session.principal.id,
|
|
129
|
+
displayName: session.principal.displayName,
|
|
130
|
+
scopes: session.principal.scopes,
|
|
131
|
+
roles: session.principal.roles,
|
|
132
|
+
permissions: session.principal.permissions,
|
|
133
|
+
metadata: session.principal.metadata,
|
|
134
|
+
iat: nowSeconds(),
|
|
135
|
+
exp: expiresAt,
|
|
136
|
+
iss: this.config.issuer,
|
|
137
|
+
jti: randomUUID(),
|
|
138
|
+
tokenType: "access"
|
|
139
|
+
}, this.config.authSecret);
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
accessToken,
|
|
143
|
+
refreshToken: nextRefreshToken,
|
|
144
|
+
tokenType: "Bearer",
|
|
145
|
+
expiresAt: formatExpiry(expiresAt),
|
|
146
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
147
|
+
principal: session.principal
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async authenticateBearerToken(token) {
|
|
151
|
+
const payload = verifyAccessToken(token, this.config.authSecret);
|
|
152
|
+
return payload ? {
|
|
153
|
+
principal: principalFromAccessTokenPayload(payload),
|
|
154
|
+
credential: {
|
|
155
|
+
type: "access_token",
|
|
156
|
+
id: payload.jti,
|
|
157
|
+
label: payload.tokenType
|
|
158
|
+
}
|
|
159
|
+
} : null;
|
|
160
|
+
}
|
|
161
|
+
async authenticateServiceCredential(_serviceId, _secret) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
async createPersonalAccessToken(_userId, _input) {
|
|
165
|
+
throw new Error("Personal access tokens are unavailable in the memory auth provider.");
|
|
166
|
+
}
|
|
167
|
+
async listPersonalAccessTokens() {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
async revokePersonalAccessToken() {
|
|
171
|
+
}
|
|
172
|
+
async syncUserIdentity(identity) {
|
|
173
|
+
return {
|
|
174
|
+
userId: identity.providerSubject,
|
|
175
|
+
identityId: identity.providerSubject,
|
|
176
|
+
principal: {
|
|
177
|
+
id: identity.providerSubject,
|
|
178
|
+
displayName: identity.displayName ?? void 0,
|
|
179
|
+
scopes: ["auth:me"],
|
|
180
|
+
roles: ["member"],
|
|
181
|
+
permissions: ["auth:read:self"],
|
|
182
|
+
metadata: identity.profile
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async createServiceToken(_input) {
|
|
187
|
+
throw new Error("Service credentials are unavailable in the memory auth provider.");
|
|
188
|
+
}
|
|
189
|
+
async rotateServiceToken(_serviceId) {
|
|
190
|
+
throw new Error("Service credentials are unavailable in the memory auth provider.");
|
|
191
|
+
}
|
|
192
|
+
createTrustedUserAssertion(claims) {
|
|
193
|
+
return Buffer.from(JSON.stringify(claims)).toString("base64url");
|
|
194
|
+
}
|
|
195
|
+
verifyTrustedUserAssertion(assertion) {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(Buffer.from(assertion, "base64url").toString("utf8"));
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async exchangeTrustedUserAssertion(claims) {
|
|
203
|
+
const principal = {
|
|
204
|
+
id: claims.userId,
|
|
205
|
+
displayName: claims.userId,
|
|
206
|
+
scopes: ["auth:me"],
|
|
207
|
+
roles: ["member"],
|
|
208
|
+
permissions: ["auth:read:self"],
|
|
209
|
+
metadata: {
|
|
210
|
+
sessionId: claims.sessionId,
|
|
211
|
+
identityId: claims.identityId
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds;
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
accessToken: createAccessToken({
|
|
218
|
+
sub: principal.id,
|
|
219
|
+
displayName: principal.displayName,
|
|
220
|
+
scopes: principal.scopes,
|
|
221
|
+
roles: principal.roles,
|
|
222
|
+
permissions: principal.permissions,
|
|
223
|
+
metadata: principal.metadata,
|
|
224
|
+
iat: nowSeconds(),
|
|
225
|
+
exp: expiresAt,
|
|
226
|
+
iss: this.config.issuer,
|
|
227
|
+
jti: randomUUID(),
|
|
228
|
+
tokenType: "access"
|
|
229
|
+
}, this.config.authSecret),
|
|
230
|
+
tokenType: "Bearer",
|
|
231
|
+
expiresAt: formatExpiry(expiresAt),
|
|
232
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
233
|
+
principal
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
MemoryDeviceCodeAuthProvider
|
|
239
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const CONTENT_RESOURCES: readonly ["pages", "notes", "questions", "objectives", "people", "agents", "books", "templates", "knowledge_downloads"];
|
|
2
|
+
export declare const PLATFORM_RESOURCES: readonly ["users", "roles", "api_tokens", "services", "jobs", "audit", "auth", "sdk", "agent", "operations"];
|
|
3
|
+
export declare const ALL_PERMISSION_RESOURCES: readonly ["pages", "notes", "questions", "objectives", "people", "agents", "books", "templates", "knowledge_downloads", "users", "roles", "api_tokens", "services", "jobs", "audit", "auth", "sdk", "agent", "operations"];
|
|
4
|
+
export type PermissionResource = (typeof ALL_PERMISSION_RESOURCES)[number];
|
|
5
|
+
export type PermissionAction = 'read' | 'create' | 'update' | 'delete' | 'manage' | 'execute' | 'impersonate';
|
|
6
|
+
export type PermissionScope = 'self' | 'global';
|
|
7
|
+
export interface PermissionDefinition {
|
|
8
|
+
key: string;
|
|
9
|
+
resource: PermissionResource | '*';
|
|
10
|
+
action: PermissionAction | '*';
|
|
11
|
+
scope: PermissionScope | '*';
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RoleDefinition {
|
|
15
|
+
key: string;
|
|
16
|
+
description: string;
|
|
17
|
+
permissions: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function permissionKey(resource: PermissionDefinition['resource'], action: PermissionDefinition['action'], scope: PermissionDefinition['scope']): string;
|
|
20
|
+
export declare const DEFAULT_PERMISSIONS: PermissionDefinition[];
|
|
21
|
+
export declare const DEFAULT_ROLES: RoleDefinition[];
|
|
22
|
+
export declare function permissionGranted(granted: string[], required: string): boolean;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const CONTENT_RESOURCES = [
|
|
2
|
+
"pages",
|
|
3
|
+
"notes",
|
|
4
|
+
"questions",
|
|
5
|
+
"objectives",
|
|
6
|
+
"people",
|
|
7
|
+
"agents",
|
|
8
|
+
"books",
|
|
9
|
+
"templates",
|
|
10
|
+
"knowledge_downloads"
|
|
11
|
+
];
|
|
12
|
+
const PLATFORM_RESOURCES = [
|
|
13
|
+
"users",
|
|
14
|
+
"roles",
|
|
15
|
+
"api_tokens",
|
|
16
|
+
"services",
|
|
17
|
+
"jobs",
|
|
18
|
+
"audit",
|
|
19
|
+
"auth",
|
|
20
|
+
"sdk",
|
|
21
|
+
"agent",
|
|
22
|
+
"operations"
|
|
23
|
+
];
|
|
24
|
+
const ALL_PERMISSION_RESOURCES = [...CONTENT_RESOURCES, ...PLATFORM_RESOURCES];
|
|
25
|
+
function permissionKey(resource, action, scope) {
|
|
26
|
+
return `${resource}:${action}:${scope}`;
|
|
27
|
+
}
|
|
28
|
+
function contentPermission(resource, action, scope, description) {
|
|
29
|
+
return {
|
|
30
|
+
key: permissionKey(resource, action, scope),
|
|
31
|
+
resource,
|
|
32
|
+
action,
|
|
33
|
+
scope,
|
|
34
|
+
description
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const permissionDefinitions = [
|
|
38
|
+
{
|
|
39
|
+
key: permissionKey("*", "*", "*"),
|
|
40
|
+
resource: "*",
|
|
41
|
+
action: "*",
|
|
42
|
+
scope: "*",
|
|
43
|
+
description: "Full platform access."
|
|
44
|
+
},
|
|
45
|
+
contentPermission("auth", "read", "self", "Read the authenticated principal."),
|
|
46
|
+
contentPermission("api_tokens", "read", "self", "List personal API tokens."),
|
|
47
|
+
contentPermission("api_tokens", "create", "self", "Create personal API tokens."),
|
|
48
|
+
contentPermission("api_tokens", "delete", "self", "Revoke personal API tokens."),
|
|
49
|
+
contentPermission("services", "impersonate", "global", "Allow trusted web and service impersonation flows."),
|
|
50
|
+
contentPermission("services", "manage", "global", "Manage service credentials and internal service auth."),
|
|
51
|
+
contentPermission("users", "read", "global", "Read user records."),
|
|
52
|
+
contentPermission("roles", "manage", "global", "Manage role assignments."),
|
|
53
|
+
contentPermission("audit", "read", "global", "Read audit events."),
|
|
54
|
+
contentPermission("jobs", "manage", "global", "Manage internal job and worker control surfaces."),
|
|
55
|
+
contentPermission("sdk", "execute", "global", "Execute SDK routes."),
|
|
56
|
+
contentPermission("agent", "execute", "global", "Execute agent routes."),
|
|
57
|
+
contentPermission("operations", "execute", "global", "Execute workflow operation routes.")
|
|
58
|
+
];
|
|
59
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
60
|
+
permissionDefinitions.push(
|
|
61
|
+
contentPermission(resource, "read", "global", `Read ${resource}.`),
|
|
62
|
+
contentPermission(resource, "create", "global", `Create ${resource}.`),
|
|
63
|
+
contentPermission(resource, "update", "global", `Update ${resource}.`),
|
|
64
|
+
contentPermission(resource, "delete", "global", `Delete ${resource}.`)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const DEFAULT_PERMISSIONS = permissionDefinitions;
|
|
68
|
+
const DEFAULT_ROLES = [
|
|
69
|
+
{
|
|
70
|
+
key: "platform_admin",
|
|
71
|
+
description: "Full platform administration.",
|
|
72
|
+
permissions: [permissionKey("*", "*", "*")]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "market_admin",
|
|
76
|
+
description: "Manage market content and core operational surfaces.",
|
|
77
|
+
permissions: [
|
|
78
|
+
permissionKey("auth", "read", "self"),
|
|
79
|
+
permissionKey("api_tokens", "read", "self"),
|
|
80
|
+
permissionKey("api_tokens", "create", "self"),
|
|
81
|
+
permissionKey("api_tokens", "delete", "self"),
|
|
82
|
+
permissionKey("sdk", "execute", "global"),
|
|
83
|
+
permissionKey("agent", "execute", "global"),
|
|
84
|
+
permissionKey("operations", "execute", "global"),
|
|
85
|
+
permissionKey("users", "read", "global"),
|
|
86
|
+
permissionKey("audit", "read", "global"),
|
|
87
|
+
permissionKey("jobs", "manage", "global")
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "content_admin",
|
|
92
|
+
description: "Manage all content resources.",
|
|
93
|
+
permissions: [
|
|
94
|
+
permissionKey("auth", "read", "self"),
|
|
95
|
+
permissionKey("api_tokens", "read", "self"),
|
|
96
|
+
permissionKey("api_tokens", "create", "self"),
|
|
97
|
+
permissionKey("api_tokens", "delete", "self")
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "content_editor",
|
|
102
|
+
description: "Edit marketplace content.",
|
|
103
|
+
permissions: [
|
|
104
|
+
permissionKey("auth", "read", "self"),
|
|
105
|
+
permissionKey("api_tokens", "read", "self"),
|
|
106
|
+
permissionKey("api_tokens", "create", "self"),
|
|
107
|
+
permissionKey("api_tokens", "delete", "self")
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "member",
|
|
112
|
+
description: "Authenticated member with personal API tokens.",
|
|
113
|
+
permissions: [
|
|
114
|
+
permissionKey("auth", "read", "self"),
|
|
115
|
+
permissionKey("api_tokens", "read", "self"),
|
|
116
|
+
permissionKey("api_tokens", "create", "self"),
|
|
117
|
+
permissionKey("api_tokens", "delete", "self")
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "viewer",
|
|
122
|
+
description: "Read-only marketplace viewer.",
|
|
123
|
+
permissions: [permissionKey("auth", "read", "self")]
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
for (const role of DEFAULT_ROLES) {
|
|
127
|
+
if (role.key === "content_admin") {
|
|
128
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
129
|
+
role.permissions.push(
|
|
130
|
+
permissionKey(resource, "read", "global"),
|
|
131
|
+
permissionKey(resource, "create", "global"),
|
|
132
|
+
permissionKey(resource, "update", "global"),
|
|
133
|
+
permissionKey(resource, "delete", "global")
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (role.key === "content_editor") {
|
|
138
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
139
|
+
role.permissions.push(
|
|
140
|
+
permissionKey(resource, "read", "global"),
|
|
141
|
+
permissionKey(resource, "create", "global"),
|
|
142
|
+
permissionKey(resource, "update", "global")
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function permissionGranted(granted, required) {
|
|
148
|
+
return granted.includes(permissionKey("*", "*", "*")) || granted.includes(required);
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
ALL_PERMISSION_RESOURCES,
|
|
152
|
+
CONTENT_RESOURCES,
|
|
153
|
+
DEFAULT_PERMISSIONS,
|
|
154
|
+
DEFAULT_ROLES,
|
|
155
|
+
PLATFORM_RESOURCES,
|
|
156
|
+
permissionGranted,
|
|
157
|
+
permissionKey
|
|
158
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApiPrincipal } from '../types.ts';
|
|
2
|
+
export interface AccessTokenPayload {
|
|
3
|
+
sub: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
roles: string[];
|
|
7
|
+
permissions: string[];
|
|
8
|
+
iat: number;
|
|
9
|
+
exp: number;
|
|
10
|
+
iss: string;
|
|
11
|
+
jti: string;
|
|
12
|
+
tokenType: 'access' | 'service';
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare function nextOpaqueToken(prefix: string): string;
|
|
16
|
+
export declare function createAccessToken(payload: AccessTokenPayload, secret: string): string;
|
|
17
|
+
export declare function verifyAccessToken(token: string, secret: string): AccessTokenPayload | null;
|
|
18
|
+
export declare function principalFromAccessTokenPayload(payload: AccessTokenPayload): ApiPrincipal;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
function encodeBase64Url(value) {
|
|
3
|
+
return Buffer.from(value).toString("base64url");
|
|
4
|
+
}
|
|
5
|
+
function decodeBase64Url(value) {
|
|
6
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
7
|
+
}
|
|
8
|
+
function sign(input, secret) {
|
|
9
|
+
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
10
|
+
}
|
|
11
|
+
function nextOpaqueToken(prefix) {
|
|
12
|
+
return `${prefix}_${randomBytes(24).toString("base64url")}`;
|
|
13
|
+
}
|
|
14
|
+
function createAccessToken(payload, secret) {
|
|
15
|
+
const encodedPayload = encodeBase64Url(JSON.stringify(payload));
|
|
16
|
+
const encodedSignature = sign(encodedPayload, secret);
|
|
17
|
+
return `${encodedPayload}.${encodedSignature}`;
|
|
18
|
+
}
|
|
19
|
+
function verifyAccessToken(token, secret) {
|
|
20
|
+
const [encodedPayload, encodedSignature] = token.split(".");
|
|
21
|
+
if (!encodedPayload || !encodedSignature) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const expected = sign(encodedPayload, secret);
|
|
25
|
+
if (expected !== encodedSignature) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const payload = JSON.parse(decodeBase64Url(encodedPayload));
|
|
30
|
+
if (!payload.sub || !Array.isArray(payload.scopes) || !payload.exp) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (payload.exp <= Math.floor(Date.now() / 1e3)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return payload;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function principalFromAccessTokenPayload(payload) {
|
|
42
|
+
return {
|
|
43
|
+
id: payload.sub,
|
|
44
|
+
displayName: payload.displayName,
|
|
45
|
+
scopes: [...payload.scopes],
|
|
46
|
+
roles: [...payload.roles],
|
|
47
|
+
permissions: [...payload.permissions],
|
|
48
|
+
metadata: payload.metadata
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
createAccessToken,
|
|
53
|
+
nextOpaqueToken,
|
|
54
|
+
principalFromAccessTokenPayload,
|
|
55
|
+
verifyAccessToken
|
|
56
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
function parseInteger(value, fallback) {
|
|
3
|
+
if (!value) return fallback;
|
|
4
|
+
const parsed = Number.parseInt(value, 10);
|
|
5
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
6
|
+
}
|
|
7
|
+
function normalizeUrl(value) {
|
|
8
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
9
|
+
}
|
|
10
|
+
function parseCsv(value) {
|
|
11
|
+
return (value ?? "").split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function resolveBaseUrl(env, host, port) {
|
|
14
|
+
if (env.TREESEED_API_BASE_URL?.trim()) {
|
|
15
|
+
return normalizeUrl(env.TREESEED_API_BASE_URL.trim());
|
|
16
|
+
}
|
|
17
|
+
if (env.RAILWAY_PUBLIC_DOMAIN?.trim()) {
|
|
18
|
+
return normalizeUrl(`https://${env.RAILWAY_PUBLIC_DOMAIN.trim()}`);
|
|
19
|
+
}
|
|
20
|
+
return normalizeUrl(`http://${host}:${port}`);
|
|
21
|
+
}
|
|
22
|
+
function resolveApiConfig(env = process.env) {
|
|
23
|
+
const host = env.HOST?.trim() || "0.0.0.0";
|
|
24
|
+
const port = parseInteger(env.PORT, 3e3);
|
|
25
|
+
const baseUrl = resolveBaseUrl(env, host === "0.0.0.0" ? "127.0.0.1" : host, port);
|
|
26
|
+
const issuer = normalizeUrl(env.TREESEED_API_ISSUER?.trim() || baseUrl);
|
|
27
|
+
const repoRoot = resolve(env.TREESEED_API_REPO_ROOT?.trim() || process.cwd());
|
|
28
|
+
return {
|
|
29
|
+
name: env.TREESEED_API_NAME?.trim() || "@treeseed/core/api",
|
|
30
|
+
host,
|
|
31
|
+
port,
|
|
32
|
+
baseUrl,
|
|
33
|
+
issuer,
|
|
34
|
+
repoRoot,
|
|
35
|
+
authSecret: env.TREESEED_API_AUTH_SECRET?.trim() || "treeseed-api-dev-secret",
|
|
36
|
+
cloudflareAccountId: env.CLOUDFLARE_ACCOUNT_ID?.trim() || void 0,
|
|
37
|
+
cloudflareApiToken: env.CLOUDFLARE_API_TOKEN?.trim() || void 0,
|
|
38
|
+
d1DatabaseId: env.TREESEED_API_D1_DATABASE_ID?.trim() || void 0,
|
|
39
|
+
d1DatabaseName: env.TREESEED_API_D1_DATABASE_NAME?.trim() || env.SITE_DATA_DB?.trim() || void 0,
|
|
40
|
+
d1LocalPersistTo: env.TREESEED_API_D1_LOCAL_PERSIST_TO?.trim() || resolve(repoRoot, ".wrangler/state/v3/d1"),
|
|
41
|
+
webServiceId: env.TREESEED_API_WEB_SERVICE_ID?.trim() || "web",
|
|
42
|
+
webServiceSecret: env.TREESEED_API_WEB_SERVICE_SECRET?.trim() || "treeseed-web-service-dev-secret",
|
|
43
|
+
webAssertionSecret: env.TREESEED_API_WEB_ASSERTION_SECRET?.trim() || env.TREESEED_API_AUTH_SECRET?.trim() || "treeseed-web-assertion-dev-secret",
|
|
44
|
+
webExchangeTtlSeconds: parseInteger(env.TREESEED_API_WEB_EXCHANGE_TTL, 300),
|
|
45
|
+
bootstrapAdminAllowlist: parseCsv(env.TREESEED_API_BOOTSTRAP_ADMIN_ALLOWLIST),
|
|
46
|
+
accessTokenTtlSeconds: parseInteger(env.TREESEED_API_ACCESS_TOKEN_TTL, 900),
|
|
47
|
+
refreshTokenTtlSeconds: parseInteger(env.TREESEED_API_REFRESH_TOKEN_TTL, 7 * 24 * 60 * 60),
|
|
48
|
+
deviceCodeTtlSeconds: parseInteger(env.TREESEED_API_DEVICE_CODE_TTL, 10 * 60),
|
|
49
|
+
deviceCodePollIntervalSeconds: parseInteger(env.TREESEED_API_DEVICE_CODE_POLL_INTERVAL, 5),
|
|
50
|
+
templateCatalogPath: env.TREESEED_API_TEMPLATE_CATALOG_PATH?.trim() || void 0,
|
|
51
|
+
providers: {
|
|
52
|
+
auth: env.TREESEED_API_PROVIDER_AUTH?.trim() || "d1",
|
|
53
|
+
agents: {
|
|
54
|
+
execution: env.TREESEED_API_PROVIDER_AGENT_EXECUTION?.trim() || "stub",
|
|
55
|
+
queue: env.TREESEED_API_PROVIDER_AGENT_QUEUE?.trim() || "memory",
|
|
56
|
+
notification: env.TREESEED_API_PROVIDER_AGENT_NOTIFICATION?.trim() || "stub",
|
|
57
|
+
repository: env.TREESEED_API_PROVIDER_AGENT_REPOSITORY?.trim() || "stub",
|
|
58
|
+
verification: env.TREESEED_API_PROVIDER_AGENT_VERIFICATION?.trim() || "stub"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
resolveApiConfig
|
|
65
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { AgentSdk } from "@treeseed/sdk";
|
|
3
|
+
import { registerAgentRoutes } from "./agent-routes.js";
|
|
4
|
+
import { bearerTokenFromRequest, jsonError } from "./http.js";
|
|
5
|
+
function createTreeseedGatewayApp(options) {
|
|
6
|
+
const sdk = options.sdk instanceof AgentSdk ? options.sdk : new AgentSdk();
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
app.use("*", async (c, next) => {
|
|
9
|
+
const token = bearerTokenFromRequest(c.req.raw);
|
|
10
|
+
if (token !== options.bearerToken) {
|
|
11
|
+
return jsonError(c, 401, "Unauthorized gateway request.");
|
|
12
|
+
}
|
|
13
|
+
c.set("requestId", "gateway");
|
|
14
|
+
c.set("config", null);
|
|
15
|
+
c.set("principal", null);
|
|
16
|
+
c.set("actingUser", null);
|
|
17
|
+
c.set("credential", null);
|
|
18
|
+
c.set("actorType", "service");
|
|
19
|
+
c.set("permissionGrants", []);
|
|
20
|
+
await next();
|
|
21
|
+
});
|
|
22
|
+
app.get("/healthz", (c) => c.json({ ok: true, service: "treeseed-agent-gateway" }));
|
|
23
|
+
registerAgentRoutes(app, {
|
|
24
|
+
sdk,
|
|
25
|
+
prefix: "",
|
|
26
|
+
scope: null,
|
|
27
|
+
projectId: options.projectId,
|
|
28
|
+
queueProducer: options.queueProducer,
|
|
29
|
+
defaultActor: "gateway"
|
|
30
|
+
});
|
|
31
|
+
return app;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
createTreeseedGatewayApp
|
|
35
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { ApiPrincipal, ApiScope } from '@treeseed/sdk/remote';
|
|
3
|
+
import type { AppVariables } from './types.ts';
|
|
4
|
+
export type ApiContext = Context<{
|
|
5
|
+
Variables: AppVariables;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function jsonError(c: Context, status: number, error: string, details?: Record<string, unknown>): Response & import("hono").TypedResponse<{
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
}, never, "json">;
|
|
11
|
+
export declare function bearerTokenFromRequest(request: Request): string;
|
|
12
|
+
export declare function hasScope(principal: ApiPrincipal | null, requiredScope: ApiScope): boolean;
|
|
13
|
+
export declare function requireScope(c: ApiContext, requiredScope: ApiScope): Response & import("hono").TypedResponse<{
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
}, never, "json">;
|
|
17
|
+
export declare function requireAuthentication(c: ApiContext): Response & import("hono").TypedResponse<{
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
}, never, "json">;
|
|
21
|
+
export declare function requirePermission(c: ApiContext, permission: string): Response & import("hono").TypedResponse<{
|
|
22
|
+
ok: false;
|
|
23
|
+
error: string;
|
|
24
|
+
}, never, "json">;
|
package/dist/api/http.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { permissionGranted } from "./auth/rbac.js";
|
|
2
|
+
function jsonError(c, status, error, details) {
|
|
3
|
+
return c.json({
|
|
4
|
+
ok: false,
|
|
5
|
+
error,
|
|
6
|
+
...details ?? {}
|
|
7
|
+
}, { status });
|
|
8
|
+
}
|
|
9
|
+
function bearerTokenFromRequest(request) {
|
|
10
|
+
const header = request.headers.get("authorization");
|
|
11
|
+
if (!header) return null;
|
|
12
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
13
|
+
return match?.[1] ?? null;
|
|
14
|
+
}
|
|
15
|
+
function hasScope(principal, requiredScope) {
|
|
16
|
+
return Boolean(principal && (principal.scopes.includes(requiredScope) || principal.scopes.includes("*")));
|
|
17
|
+
}
|
|
18
|
+
function requireScope(c, requiredScope) {
|
|
19
|
+
if (!hasScope(c.get("principal"), requiredScope)) {
|
|
20
|
+
return jsonError(c, 401, "Authentication required.", { requiredScope });
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function requireAuthentication(c) {
|
|
25
|
+
if (!c.get("principal")) {
|
|
26
|
+
return jsonError(c, 401, "Authentication required.");
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function requirePermission(c, permission) {
|
|
31
|
+
const principal = c.get("principal");
|
|
32
|
+
if (!principal || !permissionGranted(c.get("permissionGrants"), permission)) {
|
|
33
|
+
return jsonError(c, 403, "Permission denied.", { permission });
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
bearerTokenFromRequest,
|
|
39
|
+
hasScope,
|
|
40
|
+
jsonError,
|
|
41
|
+
requireAuthentication,
|
|
42
|
+
requirePermission,
|
|
43
|
+
requireScope
|
|
44
|
+
};
|