@treeseed/agent 0.8.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/Dockerfile +7 -0
- package/README.md +198 -0
- package/dist/agent-runtime.d.ts +17 -0
- package/dist/agent-runtime.js +117 -0
- package/dist/agents/adapters/execution.d.ts +41 -0
- package/dist/agents/adapters/execution.js +73 -0
- package/dist/agents/adapters/mutations.d.ts +22 -0
- package/dist/agents/adapters/mutations.js +30 -0
- package/dist/agents/adapters/notification.d.ts +26 -0
- package/dist/agents/adapters/notification.js +46 -0
- package/dist/agents/adapters/repository.d.ts +28 -0
- package/dist/agents/adapters/repository.js +61 -0
- package/dist/agents/adapters/research.d.ts +26 -0
- package/dist/agents/adapters/research.js +59 -0
- package/dist/agents/adapters/verification.d.ts +36 -0
- package/dist/agents/adapters/verification.js +62 -0
- package/dist/agents/cli-tools.d.ts +1 -0
- package/dist/agents/cli-tools.js +5 -0
- package/dist/agents/cli.d.ts +15 -0
- package/dist/agents/cli.js +109 -0
- package/dist/agents/contracts/messages.d.ts +88 -0
- package/dist/agents/contracts/messages.js +138 -0
- package/dist/agents/contracts/run.d.ts +21 -0
- package/dist/agents/contracts/run.js +0 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.js +5 -0
- package/dist/agents/kernel/agent-kernel.d.ts +63 -0
- package/dist/agents/kernel/agent-kernel.js +291 -0
- package/dist/agents/kernel/trigger-resolver.d.ts +19 -0
- package/dist/agents/kernel/trigger-resolver.js +157 -0
- package/dist/agents/registry-helper.d.ts +4 -0
- package/dist/agents/registry-helper.js +14 -0
- package/dist/agents/registry.d.ts +6 -0
- package/dist/agents/registry.js +98 -0
- package/dist/agents/runtime-types.d.ts +118 -0
- package/dist/agents/runtime-types.js +0 -0
- package/dist/agents/spec-loader.d.ts +18 -0
- package/dist/agents/spec-loader.js +54 -0
- package/dist/agents/spec-normalizer.d.ts +2 -0
- package/dist/agents/spec-normalizer.js +327 -0
- package/dist/agents/spec-types.d.ts +64 -0
- package/dist/agents/spec-types.js +0 -0
- package/dist/agents/testing/agents-smoke.d.ts +1 -0
- package/dist/agents/testing/agents-smoke.js +32 -0
- package/dist/agents/testing/e2e-harness.d.ts +44 -0
- package/dist/agents/testing/e2e-harness.js +503 -0
- package/dist/api/agent-routes.d.ts +13 -0
- package/dist/api/agent-routes.js +327 -0
- package/dist/api/app.d.ts +8 -0
- package/dist/api/app.js +444 -0
- package/dist/api/auth/d1-database.d.ts +3 -0
- package/dist/api/auth/d1-database.js +20 -0
- package/dist/api/auth/d1-provider.d.ts +79 -0
- package/dist/api/auth/d1-provider.js +92 -0
- package/dist/api/auth/d1-store.d.ts +114 -0
- package/dist/api/auth/d1-store.js +895 -0
- package/dist/api/auth/memory-provider.d.ts +77 -0
- package/dist/api/auth/memory-provider.js +249 -0
- package/dist/api/auth/rbac.d.ts +22 -0
- package/dist/api/auth/rbac.js +162 -0
- package/dist/api/auth/tokens.d.ts +18 -0
- package/dist/api/auth/tokens.js +56 -0
- package/dist/api/capabilities.d.ts +9 -0
- package/dist/api/capabilities.js +33 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +77 -0
- package/dist/api/http.d.ts +28 -0
- package/dist/api/http.js +51 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +20 -0
- package/dist/api/operations-routes.d.ts +11 -0
- package/dist/api/operations-routes.js +87 -0
- package/dist/api/operations.d.ts +3 -0
- package/dist/api/operations.js +26 -0
- package/dist/api/project-routes.d.ts +8 -0
- package/dist/api/project-routes.js +585 -0
- package/dist/api/providers.d.ts +2 -0
- package/dist/api/providers.js +62 -0
- package/dist/api/railway.d.ts +51 -0
- package/dist/api/railway.js +71 -0
- package/dist/api/sdk-dispatch.d.ts +5 -0
- package/dist/api/sdk-dispatch.js +13 -0
- package/dist/api/sdk-routes.d.ts +11 -0
- package/dist/api/sdk-routes.js +29 -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 +237 -0
- package/dist/api/types.js +0 -0
- package/dist/env.yaml +957 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +41 -0
- package/dist/scripts/assert-release-tag-version.d.ts +1 -0
- package/dist/scripts/assert-release-tag-version.js +20 -0
- package/dist/scripts/build-dist.d.ts +1 -0
- package/dist/scripts/build-dist.js +106 -0
- package/dist/scripts/package-tools.d.ts +1 -0
- package/dist/scripts/package-tools.js +7 -0
- package/dist/scripts/publish-package.d.ts +1 -0
- package/dist/scripts/publish-package.js +24 -0
- package/dist/scripts/release-verify.d.ts +1 -0
- package/dist/scripts/release-verify.js +152 -0
- package/dist/scripts/test-smoke.d.ts +1 -0
- package/dist/scripts/test-smoke.js +23 -0
- package/dist/scripts/treeseed-agent-api.d.ts +2 -0
- package/dist/scripts/treeseed-agent-api.js +25 -0
- package/dist/scripts/treeseed-agent-service.d.ts +2 -0
- package/dist/scripts/treeseed-agent-service.js +36 -0
- package/dist/scripts/treeseed-agents.d.ts +2 -0
- package/dist/scripts/treeseed-agents.js +13 -0
- package/dist/services/agents.d.ts +17 -0
- package/dist/services/agents.js +48 -0
- package/dist/services/common.d.ts +66 -0
- package/dist/services/common.js +212 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +19 -0
- package/dist/services/manager.d.ts +333 -0
- package/dist/services/manager.js +1368 -0
- package/dist/services/remote-runner.d.ts +30 -0
- package/dist/services/remote-runner.js +230 -0
- package/dist/services/workday-content.d.ts +53 -0
- package/dist/services/workday-content.js +190 -0
- package/dist/services/workday-manager.d.ts +391 -0
- package/dist/services/workday-manager.js +163 -0
- package/dist/services/workday-report.d.ts +238 -0
- package/dist/services/workday-report.js +17 -0
- package/dist/services/workday-start.d.ts +238 -0
- package/dist/services/workday-start.js +17 -0
- package/dist/services/worker-capacity.d.ts +58 -0
- package/dist/services/worker-capacity.js +208 -0
- package/dist/services/worker-pool-scaler.d.ts +27 -0
- package/dist/services/worker-pool-scaler.js +127 -0
- package/dist/services/worker.d.ts +19 -0
- package/dist/services/worker.js +436 -0
- package/dist/templates/github/deploy-processing.workflow.yml +119 -0
- package/package.json +136 -0
- package/templates/github/deploy-processing.workflow.yml +119 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ApiAuthProvider, ApiConfig, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims, UserIdentityProfileInput } from '../types.ts';
|
|
2
|
+
export declare class MemoryDeviceCodeAuthProvider implements ApiAuthProvider {
|
|
3
|
+
private readonly config;
|
|
4
|
+
readonly id = "memory";
|
|
5
|
+
private readonly devices;
|
|
6
|
+
private readonly refreshSessions;
|
|
7
|
+
constructor(config: ApiConfig);
|
|
8
|
+
startDeviceFlow(request: DeviceCodeStartRequest): Promise<DeviceCodeStartResponse>;
|
|
9
|
+
approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
}>;
|
|
12
|
+
pollDeviceFlow(request: DeviceCodePollRequest): Promise<DeviceCodePollResponse>;
|
|
13
|
+
refreshAccessToken(request: TokenRefreshRequest): Promise<TokenRefreshResponse>;
|
|
14
|
+
authenticateBearerToken(token: string): Promise<{
|
|
15
|
+
principal: ApiPrincipal;
|
|
16
|
+
credential: {
|
|
17
|
+
type: "access_token";
|
|
18
|
+
id: string;
|
|
19
|
+
label: "service" | "access";
|
|
20
|
+
};
|
|
21
|
+
} | null>;
|
|
22
|
+
authenticateServiceCredential(_serviceId: string, _secret: string): Promise<null>;
|
|
23
|
+
createPersonalAccessToken(_userId: string, _input: {
|
|
24
|
+
name: string;
|
|
25
|
+
scopes?: string[];
|
|
26
|
+
expiresAt?: string | null;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
id: string;
|
|
29
|
+
token: string;
|
|
30
|
+
prefix: string;
|
|
31
|
+
name: string;
|
|
32
|
+
expiresAt: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
listPersonalAccessTokens(): Promise<never[]>;
|
|
35
|
+
revokePersonalAccessToken(): Promise<void>;
|
|
36
|
+
syncUserIdentity(identity: UserIdentityProfileInput): Promise<{
|
|
37
|
+
userId: string;
|
|
38
|
+
identityId: string;
|
|
39
|
+
principal: {
|
|
40
|
+
id: string;
|
|
41
|
+
displayName: string | undefined;
|
|
42
|
+
scopes: string[];
|
|
43
|
+
roles: string[];
|
|
44
|
+
permissions: string[];
|
|
45
|
+
metadata: {
|
|
46
|
+
username: string | undefined;
|
|
47
|
+
firstName: string | undefined;
|
|
48
|
+
lastName: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}>;
|
|
52
|
+
createServiceToken(_input: {
|
|
53
|
+
serviceId: string;
|
|
54
|
+
name: string;
|
|
55
|
+
roles?: string[];
|
|
56
|
+
permissions?: string[];
|
|
57
|
+
}): Promise<{
|
|
58
|
+
id: string;
|
|
59
|
+
serviceId: string;
|
|
60
|
+
secret: string;
|
|
61
|
+
}>;
|
|
62
|
+
rotateServiceToken(_serviceId: string): Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
serviceId: string;
|
|
65
|
+
secret: string;
|
|
66
|
+
}>;
|
|
67
|
+
createTrustedUserAssertion(claims: TrustedUserAssertionClaims): string;
|
|
68
|
+
verifyTrustedUserAssertion(assertion: string): TrustedUserAssertionClaims | null;
|
|
69
|
+
exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{
|
|
70
|
+
ok: true;
|
|
71
|
+
accessToken: string;
|
|
72
|
+
tokenType: "Bearer";
|
|
73
|
+
expiresAt: string;
|
|
74
|
+
expiresInSeconds: number;
|
|
75
|
+
principal: ApiPrincipal;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
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: {
|
|
183
|
+
...identity.profile ?? {},
|
|
184
|
+
username: identity.username ?? void 0,
|
|
185
|
+
firstName: typeof identity.profile?.firstName === "string" ? identity.profile.firstName : void 0,
|
|
186
|
+
lastName: typeof identity.profile?.lastName === "string" ? identity.profile.lastName : void 0
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async createServiceToken(_input) {
|
|
192
|
+
throw new Error("Service credentials are unavailable in the memory auth provider.");
|
|
193
|
+
}
|
|
194
|
+
async rotateServiceToken(_serviceId) {
|
|
195
|
+
throw new Error("Service credentials are unavailable in the memory auth provider.");
|
|
196
|
+
}
|
|
197
|
+
createTrustedUserAssertion(claims) {
|
|
198
|
+
return Buffer.from(JSON.stringify(claims)).toString("base64url");
|
|
199
|
+
}
|
|
200
|
+
verifyTrustedUserAssertion(assertion) {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(Buffer.from(assertion, "base64url").toString("utf8"));
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async exchangeTrustedUserAssertion(claims) {
|
|
208
|
+
const principal = {
|
|
209
|
+
id: claims.userId,
|
|
210
|
+
displayName: claims.userId,
|
|
211
|
+
scopes: ["auth:me"],
|
|
212
|
+
roles: ["member"],
|
|
213
|
+
permissions: ["auth:read:self"],
|
|
214
|
+
metadata: {
|
|
215
|
+
sessionId: claims.sessionId,
|
|
216
|
+
identityId: claims.identityId,
|
|
217
|
+
teamId: claims.teamId ?? null,
|
|
218
|
+
projectId: claims.projectId ?? null,
|
|
219
|
+
membershipId: claims.membershipId ?? null,
|
|
220
|
+
teamRoles: claims.teamRoles ?? [],
|
|
221
|
+
teamCapabilities: claims.teamCapabilities ?? []
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds;
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
accessToken: createAccessToken({
|
|
228
|
+
sub: principal.id,
|
|
229
|
+
displayName: principal.displayName,
|
|
230
|
+
scopes: principal.scopes,
|
|
231
|
+
roles: principal.roles,
|
|
232
|
+
permissions: principal.permissions,
|
|
233
|
+
metadata: principal.metadata,
|
|
234
|
+
iat: nowSeconds(),
|
|
235
|
+
exp: expiresAt,
|
|
236
|
+
iss: this.config.issuer,
|
|
237
|
+
jti: randomUUID(),
|
|
238
|
+
tokenType: "access"
|
|
239
|
+
}, this.config.authSecret),
|
|
240
|
+
tokenType: "Bearer",
|
|
241
|
+
expiresAt: formatExpiry(expiresAt),
|
|
242
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
243
|
+
principal
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
MemoryDeviceCodeAuthProvider
|
|
249
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const CONTENT_RESOURCES: readonly ["pages", "notes", "questions", "objectives", "proposals", "decisions", "people", "agents", "books", "templates", "knowledge_packs", "workdays"];
|
|
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", "proposals", "decisions", "people", "agents", "books", "templates", "knowledge_packs", "workdays", "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,162 @@
|
|
|
1
|
+
const CONTENT_RESOURCES = [
|
|
2
|
+
"pages",
|
|
3
|
+
"notes",
|
|
4
|
+
"questions",
|
|
5
|
+
"objectives",
|
|
6
|
+
"proposals",
|
|
7
|
+
"decisions",
|
|
8
|
+
"people",
|
|
9
|
+
"agents",
|
|
10
|
+
"books",
|
|
11
|
+
"templates",
|
|
12
|
+
"knowledge_packs",
|
|
13
|
+
"workdays"
|
|
14
|
+
];
|
|
15
|
+
const PLATFORM_RESOURCES = [
|
|
16
|
+
"users",
|
|
17
|
+
"roles",
|
|
18
|
+
"api_tokens",
|
|
19
|
+
"services",
|
|
20
|
+
"jobs",
|
|
21
|
+
"audit",
|
|
22
|
+
"auth",
|
|
23
|
+
"sdk",
|
|
24
|
+
"agent",
|
|
25
|
+
"operations"
|
|
26
|
+
];
|
|
27
|
+
const ALL_PERMISSION_RESOURCES = [...CONTENT_RESOURCES, ...PLATFORM_RESOURCES];
|
|
28
|
+
function permissionKey(resource, action, scope) {
|
|
29
|
+
return `${resource}:${action}:${scope}`;
|
|
30
|
+
}
|
|
31
|
+
function contentPermission(resource, action, scope, description) {
|
|
32
|
+
return {
|
|
33
|
+
key: permissionKey(resource, action, scope),
|
|
34
|
+
resource,
|
|
35
|
+
action,
|
|
36
|
+
scope,
|
|
37
|
+
description
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const permissionDefinitions = [
|
|
41
|
+
{
|
|
42
|
+
key: permissionKey("*", "*", "*"),
|
|
43
|
+
resource: "*",
|
|
44
|
+
action: "*",
|
|
45
|
+
scope: "*",
|
|
46
|
+
description: "Full platform access."
|
|
47
|
+
},
|
|
48
|
+
contentPermission("auth", "read", "self", "Read the authenticated principal."),
|
|
49
|
+
contentPermission("api_tokens", "read", "self", "List personal API tokens."),
|
|
50
|
+
contentPermission("api_tokens", "create", "self", "Create personal API tokens."),
|
|
51
|
+
contentPermission("api_tokens", "delete", "self", "Revoke personal API tokens."),
|
|
52
|
+
contentPermission("services", "impersonate", "global", "Allow trusted web and service impersonation flows."),
|
|
53
|
+
contentPermission("services", "manage", "global", "Manage service credentials and internal service auth."),
|
|
54
|
+
contentPermission("users", "read", "global", "Read user records."),
|
|
55
|
+
contentPermission("users", "manage", "global", "Manage user records."),
|
|
56
|
+
contentPermission("roles", "manage", "global", "Manage role assignments."),
|
|
57
|
+
contentPermission("audit", "read", "global", "Read audit events."),
|
|
58
|
+
contentPermission("jobs", "manage", "global", "Manage internal job and worker control surfaces."),
|
|
59
|
+
contentPermission("sdk", "execute", "global", "Execute SDK routes."),
|
|
60
|
+
contentPermission("agent", "execute", "global", "Execute agent routes."),
|
|
61
|
+
contentPermission("operations", "execute", "global", "Execute workflow operation routes.")
|
|
62
|
+
];
|
|
63
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
64
|
+
permissionDefinitions.push(
|
|
65
|
+
contentPermission(resource, "read", "global", `Read ${resource}.`),
|
|
66
|
+
contentPermission(resource, "create", "global", `Create ${resource}.`),
|
|
67
|
+
contentPermission(resource, "update", "global", `Update ${resource}.`),
|
|
68
|
+
contentPermission(resource, "delete", "global", `Delete ${resource}.`)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const DEFAULT_PERMISSIONS = permissionDefinitions;
|
|
72
|
+
const DEFAULT_ROLES = [
|
|
73
|
+
{
|
|
74
|
+
key: "platform_admin",
|
|
75
|
+
description: "Full platform administration.",
|
|
76
|
+
permissions: [permissionKey("*", "*", "*")]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "market_admin",
|
|
80
|
+
description: "Manage market content and core operational surfaces.",
|
|
81
|
+
permissions: [
|
|
82
|
+
permissionKey("auth", "read", "self"),
|
|
83
|
+
permissionKey("api_tokens", "read", "self"),
|
|
84
|
+
permissionKey("api_tokens", "create", "self"),
|
|
85
|
+
permissionKey("api_tokens", "delete", "self"),
|
|
86
|
+
permissionKey("sdk", "execute", "global"),
|
|
87
|
+
permissionKey("agent", "execute", "global"),
|
|
88
|
+
permissionKey("operations", "execute", "global"),
|
|
89
|
+
permissionKey("users", "read", "global"),
|
|
90
|
+
permissionKey("audit", "read", "global"),
|
|
91
|
+
permissionKey("jobs", "manage", "global")
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "content_admin",
|
|
96
|
+
description: "Manage all content resources.",
|
|
97
|
+
permissions: [
|
|
98
|
+
permissionKey("auth", "read", "self"),
|
|
99
|
+
permissionKey("api_tokens", "read", "self"),
|
|
100
|
+
permissionKey("api_tokens", "create", "self"),
|
|
101
|
+
permissionKey("api_tokens", "delete", "self")
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "content_editor",
|
|
106
|
+
description: "Edit marketplace content.",
|
|
107
|
+
permissions: [
|
|
108
|
+
permissionKey("auth", "read", "self"),
|
|
109
|
+
permissionKey("api_tokens", "read", "self"),
|
|
110
|
+
permissionKey("api_tokens", "create", "self"),
|
|
111
|
+
permissionKey("api_tokens", "delete", "self")
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "member",
|
|
116
|
+
description: "Authenticated member with personal API tokens.",
|
|
117
|
+
permissions: [
|
|
118
|
+
permissionKey("auth", "read", "self"),
|
|
119
|
+
permissionKey("api_tokens", "read", "self"),
|
|
120
|
+
permissionKey("api_tokens", "create", "self"),
|
|
121
|
+
permissionKey("api_tokens", "delete", "self")
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "viewer",
|
|
126
|
+
description: "Read-only marketplace viewer.",
|
|
127
|
+
permissions: [permissionKey("auth", "read", "self")]
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
for (const role of DEFAULT_ROLES) {
|
|
131
|
+
if (role.key === "content_admin") {
|
|
132
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
133
|
+
role.permissions.push(
|
|
134
|
+
permissionKey(resource, "read", "global"),
|
|
135
|
+
permissionKey(resource, "create", "global"),
|
|
136
|
+
permissionKey(resource, "update", "global"),
|
|
137
|
+
permissionKey(resource, "delete", "global")
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (role.key === "content_editor") {
|
|
142
|
+
for (const resource of CONTENT_RESOURCES) {
|
|
143
|
+
role.permissions.push(
|
|
144
|
+
permissionKey(resource, "read", "global"),
|
|
145
|
+
permissionKey(resource, "create", "global"),
|
|
146
|
+
permissionKey(resource, "update", "global")
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function permissionGranted(granted, required) {
|
|
152
|
+
return granted.includes(permissionKey("*", "*", "*")) || granted.includes(required);
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
ALL_PERMISSION_RESOURCES,
|
|
156
|
+
CONTENT_RESOURCES,
|
|
157
|
+
DEFAULT_PERMISSIONS,
|
|
158
|
+
DEFAULT_ROLES,
|
|
159
|
+
PLATFORM_RESOURCES,
|
|
160
|
+
permissionGranted,
|
|
161
|
+
permissionKey
|
|
162
|
+
};
|
|
@@ -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,9 @@
|
|
|
1
|
+
import type { ApiPrincipal } from '@treeseed/sdk/remote';
|
|
2
|
+
import { type ApiContext } from './http.ts';
|
|
3
|
+
export declare function principalTeamCapabilities(principal: ApiPrincipal | null): string[];
|
|
4
|
+
export declare function principalTeamRoles(principal: ApiPrincipal | null): string[];
|
|
5
|
+
export declare function hasTeamCapability(principal: ApiPrincipal | null, capability: string): boolean;
|
|
6
|
+
export declare function requireTeamCapability(c: ApiContext, capability: string): (Response & import("hono").TypedResponse<{
|
|
7
|
+
ok: false;
|
|
8
|
+
error: string;
|
|
9
|
+
}, never, "json">) | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsonError } from "./http.js";
|
|
2
|
+
function stringArray(value) {
|
|
3
|
+
return Array.isArray(value) ? [...new Set(value.filter((entry) => typeof entry === "string" && entry.trim().length > 0))] : [];
|
|
4
|
+
}
|
|
5
|
+
function principalTeamCapabilities(principal) {
|
|
6
|
+
return stringArray(principal?.metadata?.teamCapabilities);
|
|
7
|
+
}
|
|
8
|
+
function principalTeamRoles(principal) {
|
|
9
|
+
return stringArray(principal?.metadata?.teamRoles);
|
|
10
|
+
}
|
|
11
|
+
function hasTeamCapability(principal, capability) {
|
|
12
|
+
if (!principal) return false;
|
|
13
|
+
if (principal.permissions.includes("*:*:*") || principal.roles.includes("project_api") || principal.roles.includes("platform_admin")) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const capabilities = principalTeamCapabilities(principal);
|
|
17
|
+
if (capabilities.includes(capability)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return principalTeamRoles(principal).includes("team_owner");
|
|
21
|
+
}
|
|
22
|
+
function requireTeamCapability(c, capability) {
|
|
23
|
+
if (!hasTeamCapability(c.get("principal"), capability)) {
|
|
24
|
+
return jsonError(c, 403, "Permission denied.", { capability });
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
hasTeamCapability,
|
|
30
|
+
principalTeamCapabilities,
|
|
31
|
+
principalTeamRoles,
|
|
32
|
+
requireTeamCapability
|
|
33
|
+
};
|