@arcblock/did-connect-service 4.0.4 → 4.0.6
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/assets/fonts/noto-sans-sc-regular.otf +0 -0
- package/dist/embedded.d.ts +32 -0
- package/dist/embedded.d.ts.map +1 -1
- package/dist/embedded.js +3 -0
- package/dist/embedded.js.map +1 -1
- package/dist/handlers/auth-handler.d.ts +5 -0
- package/dist/handlers/auth-handler.d.ts.map +1 -1
- package/dist/handlers/auth-handler.js +33 -0
- package/dist/handlers/auth-handler.js.map +1 -1
- package/dist/handlers/branding-handler.d.ts +17 -0
- package/dist/handlers/branding-handler.d.ts.map +1 -1
- package/dist/handlers/branding-handler.js +107 -5
- package/dist/handlers/branding-handler.js.map +1 -1
- package/dist/identity/csrf.d.ts +17 -0
- package/dist/identity/csrf.d.ts.map +1 -0
- package/dist/identity/csrf.js +56 -0
- package/dist/identity/csrf.js.map +1 -0
- package/dist/og/emoji.d.ts +12 -0
- package/dist/og/emoji.d.ts.map +1 -0
- package/dist/og/emoji.js +71 -0
- package/dist/og/emoji.js.map +1 -0
- package/dist/og/generator.d.ts +3 -0
- package/dist/og/generator.d.ts.map +1 -0
- package/dist/og/generator.js +338 -0
- package/dist/og/generator.js.map +1 -0
- package/dist/og/index.d.ts +6 -0
- package/dist/og/index.d.ts.map +1 -0
- package/dist/og/index.js +4 -0
- package/dist/og/index.js.map +1 -0
- package/dist/og/passport-svg.d.ts +52 -0
- package/dist/og/passport-svg.d.ts.map +1 -0
- package/dist/og/passport-svg.js +157 -0
- package/dist/og/passport-svg.js.map +1 -0
- package/dist/og/ssrf-guard.d.ts +38 -0
- package/dist/og/ssrf-guard.d.ts.map +1 -0
- package/dist/og/ssrf-guard.js +188 -0
- package/dist/og/ssrf-guard.js.map +1 -0
- package/dist/og/templates.d.ts +26 -0
- package/dist/og/templates.d.ts.map +1 -0
- package/dist/og/templates.js +302 -0
- package/dist/og/templates.js.map +1 -0
- package/dist/og/types.d.ts +74 -0
- package/dist/og/types.d.ts.map +1 -0
- package/dist/og/types.js +14 -0
- package/dist/og/types.js.map +1 -0
- package/package.json +18 -4
- package/dist/access-key-handler.d.ts +0 -37
- package/dist/access-key-handler.d.ts.map +0 -1
- package/dist/access-key-handler.js +0 -316
- package/dist/access-key-handler.js.map +0 -1
- package/dist/access-key-util.d.ts +0 -19
- package/dist/access-key-util.d.ts.map +0 -1
- package/dist/access-key-util.js +0 -45
- package/dist/access-key-util.js.map +0 -1
- package/dist/access-policy.d.ts +0 -53
- package/dist/access-policy.d.ts.map +0 -1
- package/dist/access-policy.js +0 -153
- package/dist/access-policy.js.map +0 -1
- package/dist/auth-client.d.ts +0 -20
- package/dist/auth-client.d.ts.map +0 -1
- package/dist/auth-client.js +0 -42
- package/dist/auth-client.js.map +0 -1
- package/dist/auth-entrypoint.d.ts +0 -45
- package/dist/auth-entrypoint.d.ts.map +0 -1
- package/dist/auth-entrypoint.js +0 -31
- package/dist/auth-entrypoint.js.map +0 -1
- package/dist/auth-handler.d.ts +0 -136
- package/dist/auth-handler.d.ts.map +0 -1
- package/dist/auth-handler.js +0 -408
- package/dist/auth-handler.js.map +0 -1
- package/dist/auth-rpc-types.d.ts +0 -139
- package/dist/auth-rpc-types.d.ts.map +0 -1
- package/dist/auth-rpc-types.js +0 -11
- package/dist/auth-rpc-types.js.map +0 -1
- package/dist/auth-rpc.d.ts +0 -80
- package/dist/auth-rpc.d.ts.map +0 -1
- package/dist/auth-rpc.js +0 -257
- package/dist/auth-rpc.js.map +0 -1
- package/dist/auth-worker.d.ts +0 -42
- package/dist/auth-worker.d.ts.map +0 -1
- package/dist/auth-worker.js +0 -120
- package/dist/auth-worker.js.map +0 -1
- package/dist/blocklet-js-handler.d.ts +0 -22
- package/dist/blocklet-js-handler.d.ts.map +0 -1
- package/dist/blocklet-js-handler.js +0 -205
- package/dist/blocklet-js-handler.js.map +0 -1
- package/dist/branding-handler.d.ts +0 -42
- package/dist/branding-handler.d.ts.map +0 -1
- package/dist/branding-handler.js +0 -326
- package/dist/branding-handler.js.map +0 -1
- package/dist/d1-token-storage.d.ts +0 -31
- package/dist/d1-token-storage.d.ts.map +0 -1
- package/dist/d1-token-storage.js +0 -83
- package/dist/d1-token-storage.js.map +0 -1
- package/dist/did-connect-handler.d.ts +0 -57
- package/dist/did-connect-handler.d.ts.map +0 -1
- package/dist/did-connect-handler.js +0 -182
- package/dist/did-connect-handler.js.map +0 -1
- package/dist/did.d.ts +0 -14
- package/dist/did.d.ts.map +0 -1
- package/dist/did.js +0 -17
- package/dist/did.js.map +0 -1
- package/dist/email-login-handler.d.ts +0 -50
- package/dist/email-login-handler.d.ts.map +0 -1
- package/dist/email-login-handler.js +0 -238
- package/dist/email-login-handler.js.map +0 -1
- package/dist/federation-utils.d.ts +0 -23
- package/dist/federation-utils.d.ts.map +0 -1
- package/dist/federation-utils.js +0 -25
- package/dist/federation-utils.js.map +0 -1
- package/dist/handler.d.ts +0 -90
- package/dist/handler.d.ts.map +0 -1
- package/dist/handler.js +0 -591
- package/dist/handler.js.map +0 -1
- package/dist/identity/invitation-util.d.ts +0 -7
- package/dist/identity/invitation-util.d.ts.map +0 -1
- package/dist/identity/invitation-util.js +0 -66
- package/dist/identity/invitation-util.js.map +0 -1
- package/dist/instance-role.d.ts +0 -10
- package/dist/instance-role.d.ts.map +0 -1
- package/dist/instance-role.js +0 -20
- package/dist/instance-role.js.map +0 -1
- package/dist/jwt.d.ts +0 -7
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js +0 -72
- package/dist/jwt.js.map +0 -1
- package/dist/login-entry.d.ts +0 -9
- package/dist/login-entry.d.ts.map +0 -1
- package/dist/login-entry.js +0 -9
- package/dist/login-entry.js.map +0 -1
- package/dist/membership-handler.d.ts +0 -27
- package/dist/membership-handler.d.ts.map +0 -1
- package/dist/membership-handler.js +0 -111
- package/dist/membership-handler.js.map +0 -1
- package/dist/oauth-callback-page.d.ts +0 -9
- package/dist/oauth-callback-page.d.ts.map +0 -1
- package/dist/oauth-callback-page.js +0 -31
- package/dist/oauth-callback-page.js.map +0 -1
- package/dist/oauth-handler.d.ts +0 -72
- package/dist/oauth-handler.d.ts.map +0 -1
- package/dist/oauth-handler.js +0 -423
- package/dist/oauth-handler.js.map +0 -1
- package/dist/page.d.ts +0 -33
- package/dist/page.d.ts.map +0 -1
- package/dist/page.js +0 -59
- package/dist/page.js.map +0 -1
- package/dist/pages/auth-script.d.ts +0 -18
- package/dist/pages/auth-script.d.ts.map +0 -1
- package/dist/pages/auth-script.js +0 -185
- package/dist/pages/auth-script.js.map +0 -1
- package/dist/pages/design-tokens.d.ts +0 -86
- package/dist/pages/design-tokens.d.ts.map +0 -1
- package/dist/pages/design-tokens.js +0 -159
- package/dist/pages/design-tokens.js.map +0 -1
- package/dist/pages/did-connect-script.d.ts +0 -16
- package/dist/pages/did-connect-script.d.ts.map +0 -1
- package/dist/pages/did-connect-script.js +0 -105
- package/dist/pages/did-connect-script.js.map +0 -1
- package/dist/pages/shared-styles.d.ts +0 -6
- package/dist/pages/shared-styles.d.ts.map +0 -1
- package/dist/pages/shared-styles.js +0 -109
- package/dist/pages/shared-styles.js.map +0 -1
- package/dist/rbac.d.ts +0 -19
- package/dist/rbac.d.ts.map +0 -1
- package/dist/rbac.js +0 -76
- package/dist/rbac.js.map +0 -1
- package/dist/session-context.d.ts +0 -35
- package/dist/session-context.d.ts.map +0 -1
- package/dist/session-context.js +0 -39
- package/dist/session-context.js.map +0 -1
- package/dist/store.d.ts +0 -222
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -1366
- package/dist/store.js.map +0 -1
- package/dist/team-handler.d.ts +0 -90
- package/dist/team-handler.d.ts.map +0 -1
- package/dist/team-handler.js +0 -1225
- package/dist/team-handler.js.map +0 -1
- package/dist/ticket-handler.d.ts +0 -28
- package/dist/ticket-handler.d.ts.map +0 -1
- package/dist/ticket-handler.js +0 -74
- package/dist/ticket-handler.js.map +0 -1
- package/dist/wallet-identity.d.ts +0 -32
- package/dist/wallet-identity.d.ts.map +0 -1
- package/dist/wallet-identity.js +0 -43
- package/dist/wallet-identity.js.map +0 -1
- package/dist/webauthn.d.ts +0 -65
- package/dist/webauthn.d.ts.map +0 -1
- package/dist/webauthn.js +0 -112
- package/dist/webauthn.js.map +0 -1
package/dist/team-handler.js
DELETED
|
@@ -1,1225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TeamHandler — HTTP handler for team management API + page routes.
|
|
3
|
-
*
|
|
4
|
-
* API routes (/.well-known/service/api/team/*):
|
|
5
|
-
* GET /members, /members/:did
|
|
6
|
-
* PUT /members/:did/role, /members/:did/approval
|
|
7
|
-
* DELETE /members/:did
|
|
8
|
-
* GET /invitations
|
|
9
|
-
* POST /invitations
|
|
10
|
-
* DELETE /invitations/:id
|
|
11
|
-
* GET /invitations/:id/info (public, no auth)
|
|
12
|
-
* POST /invitations/:id/accept (authenticated)
|
|
13
|
-
* GET /audit-logs
|
|
14
|
-
* GET /profile
|
|
15
|
-
* PUT /profile
|
|
16
|
-
* POST /transfer-ownership
|
|
17
|
-
*
|
|
18
|
-
* Page routes:
|
|
19
|
-
* GET /.well-known/service/admin/team → admin SPA
|
|
20
|
-
* GET /.well-known/service/invite → invite page
|
|
21
|
-
*/
|
|
22
|
-
import { resolveAccessKeyCaller } from "./handlers/passkey-handler.js";
|
|
23
|
-
import { resolveInstanceRole } from "./identity/instance-role.js";
|
|
24
|
-
import { buildAdminPageHTML } from "./pages/admin/index.js";
|
|
25
|
-
import { buildInvitePageHTML } from "./pages/invite-page.js";
|
|
26
|
-
import { PermissionError, requirePermission } from "./access/rbac.js";
|
|
27
|
-
const API_BASE = "/.well-known/service/api/team";
|
|
28
|
-
const ADMIN_PAGE = "/.well-known/service/admin/team";
|
|
29
|
-
const INVITE_PAGE = "/.well-known/service/invite";
|
|
30
|
-
export class TeamHandler {
|
|
31
|
-
store;
|
|
32
|
-
passkey;
|
|
33
|
-
apiBase;
|
|
34
|
-
portalUrl;
|
|
35
|
-
instanceDid;
|
|
36
|
-
constructor(options) {
|
|
37
|
-
this.store = options.store;
|
|
38
|
-
this.passkey = options.passkey;
|
|
39
|
-
this.apiBase = options.basePath ?? API_BASE;
|
|
40
|
-
this.portalUrl = options.portalUrl ?? "";
|
|
41
|
-
this.instanceDid = options.instanceDid;
|
|
42
|
-
}
|
|
43
|
-
/** Resolve the portal base URL for building invitation links. */
|
|
44
|
-
resolvePortalUrl(request) {
|
|
45
|
-
if (this.portalUrl)
|
|
46
|
-
return this.portalUrl;
|
|
47
|
-
return new URL(request.url).origin;
|
|
48
|
-
}
|
|
49
|
-
/** Main HTTP router. Returns Response or null if path doesn't match. */
|
|
50
|
-
async fetch(request, instanceDid) {
|
|
51
|
-
const url = new URL(request.url);
|
|
52
|
-
const { pathname } = url;
|
|
53
|
-
// API routes
|
|
54
|
-
if (pathname.startsWith(this.apiBase)) {
|
|
55
|
-
const path = pathname.slice(this.apiBase.length) || "/";
|
|
56
|
-
return this.handleAPI(request, path, url, instanceDid);
|
|
57
|
-
}
|
|
58
|
-
// Page routes (Phase 2+)
|
|
59
|
-
// For now, return null for page routes — they'll be added later
|
|
60
|
-
if (pathname === ADMIN_PAGE || pathname.startsWith(`${ADMIN_PAGE}/`)) {
|
|
61
|
-
return this.serveAdminPage(request, instanceDid);
|
|
62
|
-
}
|
|
63
|
-
if (pathname === INVITE_PAGE) {
|
|
64
|
-
return this.serveInvitePage();
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
// ─── API Router ──────────────────────────────────────────────────────
|
|
69
|
-
async handleAPI(request, path, url, instanceDid) {
|
|
70
|
-
const method = request.method;
|
|
71
|
-
try {
|
|
72
|
-
// Public endpoint: invitation info (no auth required)
|
|
73
|
-
const infoMatch = path.match(/^\/invitations\/([^/]+)\/info$/);
|
|
74
|
-
if (method === "GET" && infoMatch) {
|
|
75
|
-
return await this.handleGetInvitationInfo(infoMatch[1]);
|
|
76
|
-
}
|
|
77
|
-
// All other endpoints require authentication
|
|
78
|
-
const caller = await this.verifyAndCheckApproval(request, instanceDid);
|
|
79
|
-
// Profile (any authenticated) — not applicable in instance mode
|
|
80
|
-
if (method === "GET" && path === "/profile") {
|
|
81
|
-
return await this.handleGetProfile(caller);
|
|
82
|
-
}
|
|
83
|
-
if (method === "PUT" && path === "/profile") {
|
|
84
|
-
return await this.handleUpdateProfile(caller, request);
|
|
85
|
-
}
|
|
86
|
-
// Members (admin+)
|
|
87
|
-
if (method === "GET" && path === "/members") {
|
|
88
|
-
return await this.handleListMembers(caller, url, instanceDid);
|
|
89
|
-
}
|
|
90
|
-
const memberMatch = path.match(/^\/members\/([^/]+)$/);
|
|
91
|
-
const roleMatch = path.match(/^\/members\/([^/]+)\/role$/);
|
|
92
|
-
const approvalMatch = path.match(/^\/members\/([^/]+)\/approval$/);
|
|
93
|
-
if (method === "PUT" && roleMatch) {
|
|
94
|
-
return await this.handleChangeRole(caller, roleMatch[1], request, instanceDid);
|
|
95
|
-
}
|
|
96
|
-
if (method === "PUT" && approvalMatch) {
|
|
97
|
-
return await this.handleToggleApproval(caller, approvalMatch[1], request, instanceDid);
|
|
98
|
-
}
|
|
99
|
-
if (method === "GET" && memberMatch) {
|
|
100
|
-
return await this.handleGetMember(caller, memberMatch[1], instanceDid);
|
|
101
|
-
}
|
|
102
|
-
if (method === "DELETE" && memberMatch) {
|
|
103
|
-
return await this.handleRemoveMember(caller, memberMatch[1], instanceDid);
|
|
104
|
-
}
|
|
105
|
-
// Invitations
|
|
106
|
-
if (method === "GET" && path === "/invitations") {
|
|
107
|
-
return await this.handleListInvitations(caller, url, request, instanceDid);
|
|
108
|
-
}
|
|
109
|
-
if (method === "POST" && path === "/invitations") {
|
|
110
|
-
return await this.handleCreateInvitation(caller, request, instanceDid);
|
|
111
|
-
}
|
|
112
|
-
const acceptMatch = path.match(/^\/invitations\/([^/]+)\/accept$/);
|
|
113
|
-
if (method === "POST" && acceptMatch) {
|
|
114
|
-
return await this.handleAcceptInvitation(caller, acceptMatch[1], instanceDid);
|
|
115
|
-
}
|
|
116
|
-
const deleteInvMatch = path.match(/^\/invitations\/([^/]+)$/);
|
|
117
|
-
if (method === "DELETE" && deleteInvMatch) {
|
|
118
|
-
return await this.handleDeleteInvitation(caller, deleteInvMatch[1], instanceDid);
|
|
119
|
-
}
|
|
120
|
-
// Audit logs
|
|
121
|
-
if (method === "GET" && path === "/audit-logs") {
|
|
122
|
-
return await this.handleListAuditLogs(caller, url, instanceDid);
|
|
123
|
-
}
|
|
124
|
-
// Ownership transfer
|
|
125
|
-
if (method === "POST" && path === "/transfer-ownership") {
|
|
126
|
-
return await this.handleTransferOwnership(caller, request, instanceDid);
|
|
127
|
-
}
|
|
128
|
-
// Access policies
|
|
129
|
-
if (method === "GET" && path === "/access-policies") {
|
|
130
|
-
return await this.handleListAccessPolicies(caller, instanceDid);
|
|
131
|
-
}
|
|
132
|
-
if (method === "POST" && path === "/access-policies") {
|
|
133
|
-
return await this.handleCreateAccessPolicy(caller, request, instanceDid);
|
|
134
|
-
}
|
|
135
|
-
const policyMatch = path.match(/^\/access-policies\/([^/]+)$/);
|
|
136
|
-
if (method === "PUT" && policyMatch) {
|
|
137
|
-
return await this.handleUpdateAccessPolicy(caller, policyMatch[1], request, instanceDid);
|
|
138
|
-
}
|
|
139
|
-
if (method === "DELETE" && policyMatch) {
|
|
140
|
-
return await this.handleDeleteAccessPolicy(caller, policyMatch[1], instanceDid);
|
|
141
|
-
}
|
|
142
|
-
// Security rules
|
|
143
|
-
if (method === "GET" && path === "/security-rules") {
|
|
144
|
-
return await this.handleListSecurityRules(caller, instanceDid);
|
|
145
|
-
}
|
|
146
|
-
if (method === "POST" && path === "/security-rules") {
|
|
147
|
-
return await this.handleCreateSecurityRule(caller, request, instanceDid);
|
|
148
|
-
}
|
|
149
|
-
const ruleMatch = path.match(/^\/security-rules\/([^/]+)$/);
|
|
150
|
-
if (method === "PUT" && ruleMatch) {
|
|
151
|
-
return await this.handleUpdateSecurityRule(caller, ruleMatch[1], request, instanceDid);
|
|
152
|
-
}
|
|
153
|
-
if (method === "DELETE" && ruleMatch) {
|
|
154
|
-
return await this.handleDeleteSecurityRule(caller, ruleMatch[1], instanceDid);
|
|
155
|
-
}
|
|
156
|
-
// Settings (owner-only)
|
|
157
|
-
if (method === "GET" && path === "/settings") {
|
|
158
|
-
return await this.handleGetSettings(caller);
|
|
159
|
-
}
|
|
160
|
-
const oauthMatch = path.match(/^\/settings\/oauth\/([a-z0-9-]+)$/);
|
|
161
|
-
if (method === "PUT" && oauthMatch) {
|
|
162
|
-
return await this.handleSaveOAuthProvider(caller, oauthMatch[1], request);
|
|
163
|
-
}
|
|
164
|
-
if (method === "DELETE" && oauthMatch) {
|
|
165
|
-
return await this.handleDeleteOAuthProvider(caller, oauthMatch[1]);
|
|
166
|
-
}
|
|
167
|
-
if (method === "PUT" && path === "/settings/builtin-providers") {
|
|
168
|
-
return await this.handleSaveBuiltinProviders(caller, request);
|
|
169
|
-
}
|
|
170
|
-
if (method === "PUT" && path === "/settings/session") {
|
|
171
|
-
return await this.handleSaveSessionConfig(caller, request);
|
|
172
|
-
}
|
|
173
|
-
if (method === "PUT" && path === "/settings/email") {
|
|
174
|
-
return await this.handleSaveEmailConfig(caller, request);
|
|
175
|
-
}
|
|
176
|
-
return this.errorResponse("Not found", 404, "NOT_FOUND");
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
if (err instanceof PermissionError) {
|
|
180
|
-
return this.errorResponse("Insufficient permissions", 403, "FORBIDDEN");
|
|
181
|
-
}
|
|
182
|
-
if (err instanceof AuthError) {
|
|
183
|
-
return this.errorResponse(err.message, err.status, err.code);
|
|
184
|
-
}
|
|
185
|
-
const message = err instanceof Error ? err.message : "Internal error";
|
|
186
|
-
return this.errorResponse(message, 500, "INTERNAL_ERROR");
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// ─── Auth middleware ─────────────────────────────────────────────────
|
|
190
|
-
async verifyAndCheckApproval(request, instanceDid) {
|
|
191
|
-
// Try access key auth first
|
|
192
|
-
const akCaller = await resolveAccessKeyCaller(request, this.store, instanceDid);
|
|
193
|
-
if (akCaller) {
|
|
194
|
-
if (akCaller.blocked) {
|
|
195
|
-
throw new AuthError("User is blocked", 403, "BLOCKED");
|
|
196
|
-
}
|
|
197
|
-
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
198
|
-
if (instanceDid) {
|
|
199
|
-
const effectiveRole = await resolveInstanceRole(this.store, akCaller.did, instanceDid, akCaller.role);
|
|
200
|
-
if (!effectiveRole) {
|
|
201
|
-
throw new AuthError("Not a member of this instance", 403, "FORBIDDEN");
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
did: akCaller.did,
|
|
205
|
-
pk: akCaller.pk,
|
|
206
|
-
displayName: akCaller.displayName,
|
|
207
|
-
role: effectiveRole,
|
|
208
|
-
ip,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
did: akCaller.did,
|
|
213
|
-
pk: akCaller.pk,
|
|
214
|
-
displayName: akCaller.displayName,
|
|
215
|
-
role: akCaller.role,
|
|
216
|
-
ip,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
// Fall back to JWT auth
|
|
220
|
-
const caller = await this.passkey.verify(request);
|
|
221
|
-
if (!caller) {
|
|
222
|
-
throw new AuthError("Authentication required", 401, "UNAUTHENTICATED");
|
|
223
|
-
}
|
|
224
|
-
const user = await this.store.getUserByDid(caller.did);
|
|
225
|
-
if (!user) {
|
|
226
|
-
throw new AuthError("User not found", 401, "UNAUTHENTICATED");
|
|
227
|
-
}
|
|
228
|
-
if (!user.approved) {
|
|
229
|
-
throw new AuthError("User is blocked", 403, "BLOCKED");
|
|
230
|
-
}
|
|
231
|
-
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
232
|
-
if (instanceDid) {
|
|
233
|
-
const effectiveRole = await resolveInstanceRole(this.store, caller.did, instanceDid, user.role ?? undefined);
|
|
234
|
-
if (!effectiveRole) {
|
|
235
|
-
throw new AuthError("Not a member of this instance", 403, "FORBIDDEN");
|
|
236
|
-
}
|
|
237
|
-
return { ...caller, role: effectiveRole, ip };
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
...caller,
|
|
241
|
-
role: user.role ?? "guest",
|
|
242
|
-
ip,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
// ─── Profile handlers ───────────────────────────────────────────────
|
|
246
|
-
async handleGetProfile(caller) {
|
|
247
|
-
const user = await this.store.getMemberInfo(caller.did);
|
|
248
|
-
if (!user) {
|
|
249
|
-
return this.errorResponse("User not found", 404, "NOT_FOUND");
|
|
250
|
-
}
|
|
251
|
-
return this.jsonResponse({ ok: true, user });
|
|
252
|
-
}
|
|
253
|
-
async handleUpdateProfile(caller, request) {
|
|
254
|
-
const body = await this.parseJSON(request);
|
|
255
|
-
const fields = {};
|
|
256
|
-
if (body.fullName !== undefined)
|
|
257
|
-
fields.fullName = String(body.fullName).slice(0, 64);
|
|
258
|
-
if (body.email !== undefined)
|
|
259
|
-
fields.email = String(body.email).slice(0, 255);
|
|
260
|
-
if (body.avatar !== undefined)
|
|
261
|
-
fields.avatar = String(body.avatar).slice(0, 1024);
|
|
262
|
-
if (Object.keys(fields).length > 0) {
|
|
263
|
-
await this.store.updateUserProfile(caller.did, fields);
|
|
264
|
-
await this.store.createAuditLog({
|
|
265
|
-
action: "user.profile_update",
|
|
266
|
-
operatorDid: caller.did,
|
|
267
|
-
targetDid: caller.did,
|
|
268
|
-
metadata: { fields: Object.keys(fields) },
|
|
269
|
-
ip: caller.ip,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
const user = await this.store.getMemberInfo(caller.did);
|
|
273
|
-
return this.jsonResponse({ ok: true, user });
|
|
274
|
-
}
|
|
275
|
-
// ─── Members handlers ──────────────────────────────────────────────
|
|
276
|
-
async handleListMembers(caller, url, instanceDid) {
|
|
277
|
-
requirePermission(caller.role, "team.list_members");
|
|
278
|
-
if (instanceDid) {
|
|
279
|
-
const members = await this.store.listMembershipsWithUserInfo(instanceDid);
|
|
280
|
-
return this.jsonResponse({
|
|
281
|
-
ok: true,
|
|
282
|
-
users: members,
|
|
283
|
-
paging: { total: members.length, page: 1, pageSize: members.length },
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
const page = Math.max(1, Number.parseInt(url.searchParams.get("page") ?? "1", 10) || 1);
|
|
287
|
-
const pageSize = Math.min(100, Math.max(1, Number.parseInt(url.searchParams.get("pageSize") ?? "20", 10) || 20));
|
|
288
|
-
const role = url.searchParams.get("role") || undefined;
|
|
289
|
-
const search = url.searchParams.get("search") || undefined;
|
|
290
|
-
const approvedParam = url.searchParams.get("approved");
|
|
291
|
-
const approved = approvedParam !== null ? Number.parseInt(approvedParam, 10) : undefined;
|
|
292
|
-
const sourceProvider = url.searchParams.get("sourceProvider") || undefined;
|
|
293
|
-
const { users, total } = await this.store.getUsers({ page, pageSize, role, search, approved, sourceProvider });
|
|
294
|
-
return this.jsonResponse({
|
|
295
|
-
ok: true,
|
|
296
|
-
users,
|
|
297
|
-
paging: { total, page, pageSize },
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
async handleGetMember(caller, did, instanceDid) {
|
|
301
|
-
requirePermission(caller.role, "team.view_member");
|
|
302
|
-
did = decodeURIComponent(did);
|
|
303
|
-
if (instanceDid) {
|
|
304
|
-
const membership = await this.store.getMembership(did, instanceDid);
|
|
305
|
-
if (!membership) {
|
|
306
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
307
|
-
}
|
|
308
|
-
const userInfo = await this.store.getUserByDid(did);
|
|
309
|
-
return this.jsonResponse({
|
|
310
|
-
ok: true,
|
|
311
|
-
user: {
|
|
312
|
-
user_did: did,
|
|
313
|
-
role: membership.role,
|
|
314
|
-
joined_at: membership.joined_at,
|
|
315
|
-
fullName: userInfo?.fullName ?? null,
|
|
316
|
-
email: userInfo?.email ?? null,
|
|
317
|
-
avatar: userInfo?.avatar ?? null,
|
|
318
|
-
instanceDid,
|
|
319
|
-
},
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
const user = await this.store.getMemberInfo(did);
|
|
323
|
-
if (!user) {
|
|
324
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
325
|
-
}
|
|
326
|
-
return this.jsonResponse({ ok: true, user });
|
|
327
|
-
}
|
|
328
|
-
async handleChangeRole(caller, targetDid, request, instanceDid) {
|
|
329
|
-
requirePermission(caller.role, "team.change_role");
|
|
330
|
-
targetDid = decodeURIComponent(targetDid);
|
|
331
|
-
const body = await this.parseJSON(request);
|
|
332
|
-
const newRole = body.role;
|
|
333
|
-
if (!newRole || !["admin", "member", "guest"].includes(newRole)) {
|
|
334
|
-
return this.errorResponse("Invalid role. Must be 'admin', 'member', or 'guest'", 400, "VALIDATION_ERROR");
|
|
335
|
-
}
|
|
336
|
-
if (targetDid === caller.did) {
|
|
337
|
-
return this.errorResponse("Cannot change your own role", 409, "CONFLICT");
|
|
338
|
-
}
|
|
339
|
-
if (instanceDid) {
|
|
340
|
-
const membership = await this.store.getMembership(targetDid, instanceDid);
|
|
341
|
-
if (!membership) {
|
|
342
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
343
|
-
}
|
|
344
|
-
const oldRole = membership.role;
|
|
345
|
-
await this.store.updateMembershipRole(targetDid, instanceDid, newRole);
|
|
346
|
-
await this.store.createAuditLog({
|
|
347
|
-
action: "user.role_change",
|
|
348
|
-
operatorDid: caller.did,
|
|
349
|
-
targetDid,
|
|
350
|
-
metadata: { oldRole, newRole },
|
|
351
|
-
ip: caller.ip,
|
|
352
|
-
instanceDid,
|
|
353
|
-
});
|
|
354
|
-
const updated = await this.store.getMembership(targetDid, instanceDid);
|
|
355
|
-
return this.jsonResponse({ ok: true, user: updated });
|
|
356
|
-
}
|
|
357
|
-
const target = await this.store.getUserByDid(targetDid);
|
|
358
|
-
if (!target) {
|
|
359
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
360
|
-
}
|
|
361
|
-
const oldRole = target.role;
|
|
362
|
-
await this.store.updateUserRole(targetDid, newRole);
|
|
363
|
-
await this.store.createAuditLog({
|
|
364
|
-
action: "user.role_change",
|
|
365
|
-
operatorDid: caller.did,
|
|
366
|
-
targetDid,
|
|
367
|
-
metadata: { oldRole, newRole },
|
|
368
|
-
ip: caller.ip,
|
|
369
|
-
});
|
|
370
|
-
const user = await this.store.getMemberInfo(targetDid);
|
|
371
|
-
return this.jsonResponse({ ok: true, user });
|
|
372
|
-
}
|
|
373
|
-
async handleToggleApproval(caller, targetDid, request, instanceDid) {
|
|
374
|
-
if (instanceDid) {
|
|
375
|
-
return this.errorResponse("Not supported for instances", 501, "NOT_IMPLEMENTED");
|
|
376
|
-
}
|
|
377
|
-
targetDid = decodeURIComponent(targetDid);
|
|
378
|
-
const body = await this.parseJSON(request);
|
|
379
|
-
if (typeof body.approved !== "boolean") {
|
|
380
|
-
return this.errorResponse("'approved' must be a boolean", 400, "VALIDATION_ERROR");
|
|
381
|
-
}
|
|
382
|
-
if (targetDid === caller.did) {
|
|
383
|
-
return this.errorResponse("Cannot change your own approval status", 409, "CONFLICT");
|
|
384
|
-
}
|
|
385
|
-
const target = await this.store.getUserByDid(targetDid);
|
|
386
|
-
if (!target) {
|
|
387
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
388
|
-
}
|
|
389
|
-
if (target.role === "owner") {
|
|
390
|
-
return this.errorResponse("Cannot block the owner", 403, "FORBIDDEN");
|
|
391
|
-
}
|
|
392
|
-
const action = body.approved ? "team.unblock_member" : "team.block_member";
|
|
393
|
-
requirePermission(caller.role, action, target.role);
|
|
394
|
-
await this.store.updateUserApproval(targetDid, body.approved);
|
|
395
|
-
await this.store.createAuditLog({
|
|
396
|
-
action: body.approved ? "user.unblock" : "user.block",
|
|
397
|
-
operatorDid: caller.did,
|
|
398
|
-
targetDid,
|
|
399
|
-
ip: caller.ip,
|
|
400
|
-
});
|
|
401
|
-
const user = await this.store.getMemberInfo(targetDid);
|
|
402
|
-
return this.jsonResponse({ ok: true, user });
|
|
403
|
-
}
|
|
404
|
-
async handleRemoveMember(caller, targetDid, instanceDid) {
|
|
405
|
-
targetDid = decodeURIComponent(targetDid);
|
|
406
|
-
if (targetDid === caller.did) {
|
|
407
|
-
return this.errorResponse("Cannot remove yourself", 409, "CONFLICT");
|
|
408
|
-
}
|
|
409
|
-
if (instanceDid) {
|
|
410
|
-
const membership = await this.store.getMembership(targetDid, instanceDid);
|
|
411
|
-
if (!membership) {
|
|
412
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
413
|
-
}
|
|
414
|
-
if (membership.role === "owner") {
|
|
415
|
-
return this.errorResponse("Cannot remove the owner", 403, "FORBIDDEN");
|
|
416
|
-
}
|
|
417
|
-
requirePermission(caller.role, "team.remove_member", membership.role);
|
|
418
|
-
await this.store.deleteMembership(targetDid, instanceDid);
|
|
419
|
-
await this.store.createAuditLog({
|
|
420
|
-
action: "user.remove",
|
|
421
|
-
operatorDid: caller.did,
|
|
422
|
-
targetDid,
|
|
423
|
-
metadata: { removedRole: membership.role },
|
|
424
|
-
ip: caller.ip,
|
|
425
|
-
instanceDid,
|
|
426
|
-
});
|
|
427
|
-
return this.jsonResponse({ ok: true });
|
|
428
|
-
}
|
|
429
|
-
const target = await this.store.getUserByDid(targetDid);
|
|
430
|
-
if (!target) {
|
|
431
|
-
return this.errorResponse("Member not found", 404, "NOT_FOUND");
|
|
432
|
-
}
|
|
433
|
-
if (target.role === "owner") {
|
|
434
|
-
return this.errorResponse("Cannot remove the owner", 403, "FORBIDDEN");
|
|
435
|
-
}
|
|
436
|
-
requirePermission(caller.role, "team.remove_member", target.role);
|
|
437
|
-
await this.store.removeUser(targetDid);
|
|
438
|
-
await this.store.createAuditLog({
|
|
439
|
-
action: "user.remove",
|
|
440
|
-
operatorDid: caller.did,
|
|
441
|
-
targetDid,
|
|
442
|
-
metadata: { removedRole: target.role, removedName: target.fullName },
|
|
443
|
-
ip: caller.ip,
|
|
444
|
-
});
|
|
445
|
-
return this.jsonResponse({ ok: true });
|
|
446
|
-
}
|
|
447
|
-
// ─── Invitation handlers ────────────────────────────────────────────
|
|
448
|
-
async handleListInvitations(caller, url, request, instanceDid) {
|
|
449
|
-
requirePermission(caller.role, "team.list_members"); // same perm as viewing members
|
|
450
|
-
const page = Math.max(1, Number.parseInt(url.searchParams.get("page") ?? "1", 10) || 1);
|
|
451
|
-
const pageSize = Math.min(100, Math.max(1, Number.parseInt(url.searchParams.get("pageSize") ?? "20", 10) || 20));
|
|
452
|
-
// Lazy-purge expired invitations
|
|
453
|
-
this.store.purgeExpiredInvitations().catch(() => { });
|
|
454
|
-
const { invitations, total } = await this.store.getInvitations({ page, pageSize, instanceDid });
|
|
455
|
-
// Add link to each invitation
|
|
456
|
-
const baseUrl = this.resolvePortalUrl(request);
|
|
457
|
-
const withLinks = invitations.map((inv) => ({
|
|
458
|
-
...inv,
|
|
459
|
-
link: `${baseUrl}/.well-known/service/invite?id=${inv.id}`,
|
|
460
|
-
}));
|
|
461
|
-
return this.jsonResponse({
|
|
462
|
-
ok: true,
|
|
463
|
-
invitations: withLinks,
|
|
464
|
-
paging: { total, page, pageSize },
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
async handleCreateInvitation(caller, request, instanceDid) {
|
|
468
|
-
requirePermission(caller.role, "team.create_invitation");
|
|
469
|
-
const body = await this.parseJSON(request);
|
|
470
|
-
if (!body.role || !["admin", "member", "guest"].includes(body.role)) {
|
|
471
|
-
return this.errorResponse("Invalid role. Must be 'admin', 'member', or 'guest'", 400, "VALIDATION_ERROR");
|
|
472
|
-
}
|
|
473
|
-
// Admin cannot create admin-role invitations
|
|
474
|
-
if (caller.role === "admin" && body.role === "admin") {
|
|
475
|
-
return this.errorResponse("Only the owner can create admin invitations", 403, "FORBIDDEN");
|
|
476
|
-
}
|
|
477
|
-
const remark = body.remark ? String(body.remark) : "";
|
|
478
|
-
if (remark.length > 500) {
|
|
479
|
-
return this.errorResponse("Remark must be 500 characters or fewer", 400, "VALIDATION_ERROR");
|
|
480
|
-
}
|
|
481
|
-
const expireHours = body.expireHours !== undefined ? Number(body.expireHours) : 168;
|
|
482
|
-
if (expireHours < 1 || expireHours > 720) {
|
|
483
|
-
return this.errorResponse("expireHours must be between 1 and 720", 400, "VALIDATION_ERROR");
|
|
484
|
-
}
|
|
485
|
-
const maxUses = body.maxUses !== undefined ? Number(body.maxUses) : 1;
|
|
486
|
-
if (maxUses < 1 || maxUses > 100) {
|
|
487
|
-
return this.errorResponse("maxUses must be between 1 and 100", 400, "VALIDATION_ERROR");
|
|
488
|
-
}
|
|
489
|
-
const invitation = await this.store.createInvitation({
|
|
490
|
-
role: body.role,
|
|
491
|
-
remark,
|
|
492
|
-
inviterDid: caller.did,
|
|
493
|
-
teamDid: caller.did, // Use caller DID as team DID for now
|
|
494
|
-
expireHours,
|
|
495
|
-
maxUses,
|
|
496
|
-
instanceDid,
|
|
497
|
-
});
|
|
498
|
-
await this.store.createAuditLog({
|
|
499
|
-
action: "invitation.create",
|
|
500
|
-
operatorDid: caller.did,
|
|
501
|
-
metadata: { role: body.role, maxUses, expireHours },
|
|
502
|
-
ip: caller.ip,
|
|
503
|
-
instanceDid,
|
|
504
|
-
});
|
|
505
|
-
const baseUrl = this.resolvePortalUrl(request);
|
|
506
|
-
const link = `${baseUrl}/.well-known/service/invite?id=${invitation.id}`;
|
|
507
|
-
// Resolve inviter name
|
|
508
|
-
const inviterUser = await this.store.getUserByDid(caller.did);
|
|
509
|
-
return this.jsonResponse({
|
|
510
|
-
ok: true,
|
|
511
|
-
invitation: {
|
|
512
|
-
...invitation,
|
|
513
|
-
inviterName: inviterUser?.fullName ?? null,
|
|
514
|
-
link,
|
|
515
|
-
},
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
async handleDeleteInvitation(caller, invitationId, instanceDid) {
|
|
519
|
-
requirePermission(caller.role, "team.delete_invitation");
|
|
520
|
-
const invitation = await this.store.getInvitation(invitationId);
|
|
521
|
-
if (!invitation) {
|
|
522
|
-
return this.errorResponse("Invitation not found", 404, "NOT_FOUND");
|
|
523
|
-
}
|
|
524
|
-
// Instance context: verify invitation belongs to this instance
|
|
525
|
-
if (instanceDid && invitation.instance_did !== instanceDid) {
|
|
526
|
-
return this.errorResponse("Invitation not found", 404, "NOT_FOUND");
|
|
527
|
-
}
|
|
528
|
-
// Admin can only delete own invitations
|
|
529
|
-
if (caller.role === "admin" && invitation.inviterDid !== caller.did) {
|
|
530
|
-
return this.errorResponse("Admin can only delete own invitations", 403, "FORBIDDEN");
|
|
531
|
-
}
|
|
532
|
-
await this.store.deleteInvitation(invitationId);
|
|
533
|
-
await this.store.createAuditLog({
|
|
534
|
-
action: "invitation.delete",
|
|
535
|
-
operatorDid: caller.did,
|
|
536
|
-
metadata: { invitationId, role: invitation.role },
|
|
537
|
-
ip: caller.ip,
|
|
538
|
-
instanceDid,
|
|
539
|
-
});
|
|
540
|
-
return this.jsonResponse({ ok: true });
|
|
541
|
-
}
|
|
542
|
-
async handleGetInvitationInfo(invitationId) {
|
|
543
|
-
const invitation = await this.store.getInvitation(invitationId);
|
|
544
|
-
if (!invitation) {
|
|
545
|
-
return this.jsonResponse({ valid: false, error: "not_found" });
|
|
546
|
-
}
|
|
547
|
-
// Check expiration
|
|
548
|
-
if (new Date(invitation.expireAt) < new Date()) {
|
|
549
|
-
return this.jsonResponse({ valid: false, error: "expired" });
|
|
550
|
-
}
|
|
551
|
-
// Check status
|
|
552
|
-
if (invitation.status === "closed" || invitation.useCount >= invitation.maxUses) {
|
|
553
|
-
return this.jsonResponse({ valid: false, error: "closed" });
|
|
554
|
-
}
|
|
555
|
-
if (invitation.status === "expired") {
|
|
556
|
-
return this.jsonResponse({ valid: false, error: "expired" });
|
|
557
|
-
}
|
|
558
|
-
return this.jsonResponse({
|
|
559
|
-
valid: true,
|
|
560
|
-
role: invitation.role,
|
|
561
|
-
remark: invitation.remark,
|
|
562
|
-
expireAt: invitation.expireAt,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
async handleAcceptInvitation(caller, invitationId, instanceDid) {
|
|
566
|
-
const invitation = await this.store.getInvitation(invitationId);
|
|
567
|
-
if (!invitation) {
|
|
568
|
-
return this.errorResponse("Invitation not found", 404, "NOT_FOUND");
|
|
569
|
-
}
|
|
570
|
-
// Instance context: verify invitation belongs to this instance
|
|
571
|
-
const invInstanceDid = invitation.instance_did;
|
|
572
|
-
if (instanceDid && invInstanceDid && invInstanceDid !== instanceDid) {
|
|
573
|
-
return this.errorResponse("Invitation not found", 404, "NOT_FOUND");
|
|
574
|
-
}
|
|
575
|
-
// Validate invitation state
|
|
576
|
-
if (new Date(invitation.expireAt) < new Date()) {
|
|
577
|
-
return this.errorResponse("Invitation has expired", 410, "INVITATION_EXPIRED");
|
|
578
|
-
}
|
|
579
|
-
if (invitation.status === "expired") {
|
|
580
|
-
return this.errorResponse("Invitation has expired", 410, "INVITATION_EXPIRED");
|
|
581
|
-
}
|
|
582
|
-
if (invitation.status === "closed" || invitation.useCount >= invitation.maxUses) {
|
|
583
|
-
return this.errorResponse("Invitation is no longer available", 410, "INVITATION_CLOSED");
|
|
584
|
-
}
|
|
585
|
-
// Atomically increment use count
|
|
586
|
-
const incremented = await this.store.incrementInvitationUseCount(invitationId);
|
|
587
|
-
if (!incremented) {
|
|
588
|
-
return this.errorResponse("Invitation is no longer available", 410, "INVITATION_CLOSED");
|
|
589
|
-
}
|
|
590
|
-
// Close invitation if max uses reached
|
|
591
|
-
const updated = await this.store.getInvitation(invitationId);
|
|
592
|
-
if (updated && updated.useCount >= updated.maxUses) {
|
|
593
|
-
await this.store.updateInvitationStatus(invitationId, "closed");
|
|
594
|
-
}
|
|
595
|
-
const invitedRole = invitation.role;
|
|
596
|
-
const ROLE_LEVEL = { owner: 3, admin: 2, member: 1, guest: 0 };
|
|
597
|
-
const invitedLevel = ROLE_LEVEL[invitedRole] ?? 0;
|
|
598
|
-
// Instance-scoped invitation → create/upgrade membership
|
|
599
|
-
// Only use the invitation's own instance_did — a system-level invitation
|
|
600
|
-
// (instance_did=NULL) accepted on an instance domain should NOT create
|
|
601
|
-
// a membership for that instance; it should fall through to system-level logic.
|
|
602
|
-
if (invInstanceDid) {
|
|
603
|
-
const existing = await this.store.getMembership(caller.did, invInstanceDid);
|
|
604
|
-
if (existing) {
|
|
605
|
-
const existingLevel = ROLE_LEVEL[existing.role] ?? 0;
|
|
606
|
-
if (invitedLevel > existingLevel) {
|
|
607
|
-
await this.store.updateMembershipRole(caller.did, invInstanceDid, invitedRole);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
await this.store.createMembership(caller.did, invInstanceDid, invitedRole, invitation.inviterDid);
|
|
612
|
-
}
|
|
613
|
-
await this.store.createAuditLog({
|
|
614
|
-
action: "user.accept_invitation",
|
|
615
|
-
operatorDid: caller.did,
|
|
616
|
-
metadata: { invitationId, role: invitedRole },
|
|
617
|
-
ip: caller.ip,
|
|
618
|
-
instanceDid: invInstanceDid,
|
|
619
|
-
});
|
|
620
|
-
const finalMembership = await this.store.getMembership(caller.did, invInstanceDid);
|
|
621
|
-
return this.jsonResponse({ ok: true, role: finalMembership?.role ?? invitedRole });
|
|
622
|
-
}
|
|
623
|
-
// System-level invitation → upgrade system role
|
|
624
|
-
const user = await this.store.getUserByDid(caller.did);
|
|
625
|
-
const currentRole = user?.role;
|
|
626
|
-
const currentLevel = ROLE_LEVEL[currentRole ?? ""] ?? 0;
|
|
627
|
-
if (invitedLevel > currentLevel) {
|
|
628
|
-
await this.store.updateUserRole(caller.did, invitedRole);
|
|
629
|
-
}
|
|
630
|
-
// Set inviter (only if not already set)
|
|
631
|
-
await this.store.setUserInviter(caller.did, invitation.inviterDid);
|
|
632
|
-
// Audit log
|
|
633
|
-
await this.store.createAuditLog({
|
|
634
|
-
action: "user.accept_invitation",
|
|
635
|
-
operatorDid: caller.did,
|
|
636
|
-
metadata: { invitationId, role: invitedRole },
|
|
637
|
-
ip: caller.ip,
|
|
638
|
-
});
|
|
639
|
-
const finalRole = invitedLevel > currentLevel ? invitedRole : (currentRole ?? invitedRole);
|
|
640
|
-
return this.jsonResponse({ ok: true, role: finalRole });
|
|
641
|
-
}
|
|
642
|
-
// ─── Audit logs handler ─────────────────────────────────────────────
|
|
643
|
-
async handleListAuditLogs(caller, url, instanceDid) {
|
|
644
|
-
requirePermission(caller.role, "team.view_audit_logs");
|
|
645
|
-
const page = Math.max(1, Number.parseInt(url.searchParams.get("page") ?? "1", 10) || 1);
|
|
646
|
-
const pageSize = Math.min(100, Math.max(1, Number.parseInt(url.searchParams.get("pageSize") ?? "50", 10) || 50));
|
|
647
|
-
const action = url.searchParams.get("action") || undefined;
|
|
648
|
-
const { logs, total } = await this.store.getAuditLogs({ page, pageSize, action, instanceDid });
|
|
649
|
-
return this.jsonResponse({
|
|
650
|
-
ok: true,
|
|
651
|
-
logs,
|
|
652
|
-
paging: { total, page, pageSize },
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
// ─── Ownership transfer handler ─────────────────────────────────────
|
|
656
|
-
async handleTransferOwnership(caller, request, instanceDid) {
|
|
657
|
-
if (instanceDid) {
|
|
658
|
-
return this.errorResponse("Not supported for instances", 501, "NOT_IMPLEMENTED");
|
|
659
|
-
}
|
|
660
|
-
requirePermission(caller.role, "team.transfer_ownership");
|
|
661
|
-
const body = await this.parseJSON(request);
|
|
662
|
-
if (!body.targetDid) {
|
|
663
|
-
return this.errorResponse("targetDid is required", 400, "VALIDATION_ERROR");
|
|
664
|
-
}
|
|
665
|
-
const targetDid = body.targetDid;
|
|
666
|
-
if (targetDid === caller.did) {
|
|
667
|
-
return this.errorResponse("Cannot transfer ownership to yourself", 400, "VALIDATION_ERROR");
|
|
668
|
-
}
|
|
669
|
-
const target = await this.store.getUserByDid(targetDid);
|
|
670
|
-
if (!target) {
|
|
671
|
-
return this.errorResponse("Target member not found", 404, "NOT_FOUND");
|
|
672
|
-
}
|
|
673
|
-
if (!target.approved) {
|
|
674
|
-
return this.errorResponse("Cannot transfer ownership to a blocked user", 400, "VALIDATION_ERROR");
|
|
675
|
-
}
|
|
676
|
-
await this.store.transferOwnership(caller.did, targetDid);
|
|
677
|
-
await this.store.createAuditLog({
|
|
678
|
-
action: "user.transfer_ownership",
|
|
679
|
-
operatorDid: caller.did,
|
|
680
|
-
targetDid,
|
|
681
|
-
metadata: { newOwner: target.fullName },
|
|
682
|
-
ip: caller.ip,
|
|
683
|
-
});
|
|
684
|
-
return this.jsonResponse({ ok: true });
|
|
685
|
-
}
|
|
686
|
-
// ─── Access policy handlers ─────────────────────────────────────────
|
|
687
|
-
async handleListAccessPolicies(caller, instanceDid) {
|
|
688
|
-
requirePermission(caller.role, "access_policy.list");
|
|
689
|
-
const policies = await this.store.getAccessPolicies(instanceDid);
|
|
690
|
-
return this.jsonResponse({ ok: true, policies });
|
|
691
|
-
}
|
|
692
|
-
async handleCreateAccessPolicy(caller, request, instanceDid) {
|
|
693
|
-
requirePermission(caller.role, "access_policy.create");
|
|
694
|
-
const body = await this.parseJSON(request);
|
|
695
|
-
if (!body.name || body.name.length > 32) {
|
|
696
|
-
return this.errorResponse("Name is required (max 32 chars)", 400, "VALIDATION_ERROR");
|
|
697
|
-
}
|
|
698
|
-
const validTypes = ["public", "invited", "owner", "admin", "roles", "roles_reverse"];
|
|
699
|
-
if (!body.accessType || !validTypes.includes(body.accessType)) {
|
|
700
|
-
return this.errorResponse("Invalid accessType", 400, "VALIDATION_ERROR");
|
|
701
|
-
}
|
|
702
|
-
if (body.accessType === "roles" || body.accessType === "roles_reverse") {
|
|
703
|
-
if (!body.roles || body.roles.length === 0) {
|
|
704
|
-
return this.errorResponse("Roles required for this access type", 400, "VALIDATION_ERROR");
|
|
705
|
-
}
|
|
706
|
-
const validRoles = ["owner", "admin", "member", "guest"];
|
|
707
|
-
if (body.roles.some((r) => !validRoles.includes(r))) {
|
|
708
|
-
return this.errorResponse("Invalid role name", 400, "VALIDATION_ERROR");
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const policy = await this.store.createAccessPolicy({
|
|
712
|
-
name: body.name,
|
|
713
|
-
description: body.description?.slice(0, 256),
|
|
714
|
-
accessType: body.accessType,
|
|
715
|
-
roles: body.roles,
|
|
716
|
-
instanceDid,
|
|
717
|
-
});
|
|
718
|
-
await this.store.createAuditLog({
|
|
719
|
-
action: "access_policy.create",
|
|
720
|
-
operatorDid: caller.did,
|
|
721
|
-
metadata: { policyId: policy.id, name: body.name },
|
|
722
|
-
ip: caller.ip,
|
|
723
|
-
instanceDid,
|
|
724
|
-
});
|
|
725
|
-
return this.jsonResponse({ ok: true, policy });
|
|
726
|
-
}
|
|
727
|
-
async handleUpdateAccessPolicy(caller, id, request, instanceDid) {
|
|
728
|
-
requirePermission(caller.role, "access_policy.update");
|
|
729
|
-
const existing = await this.store.getAccessPolicy(id);
|
|
730
|
-
if (!existing) {
|
|
731
|
-
return this.errorResponse("Policy not found", 404, "NOT_FOUND");
|
|
732
|
-
}
|
|
733
|
-
if (existing.isProtected) {
|
|
734
|
-
return this.errorResponse("Cannot update protected policies", 403, "FORBIDDEN");
|
|
735
|
-
}
|
|
736
|
-
// Instance context: can only modify own instance's policies, not global
|
|
737
|
-
if (instanceDid && existing.instanceDid === null) {
|
|
738
|
-
return this.errorResponse("Cannot modify global policies from instance context", 403, "FORBIDDEN");
|
|
739
|
-
}
|
|
740
|
-
if (instanceDid && existing.instanceDid !== null && existing.instanceDid !== instanceDid) {
|
|
741
|
-
return this.errorResponse("Policy not found", 404, "NOT_FOUND");
|
|
742
|
-
}
|
|
743
|
-
const body = await this.parseJSON(request);
|
|
744
|
-
if (body.name !== undefined && (body.name.length === 0 || body.name.length > 32)) {
|
|
745
|
-
return this.errorResponse("Name must be 1-32 chars", 400, "VALIDATION_ERROR");
|
|
746
|
-
}
|
|
747
|
-
if (body.accessType !== undefined) {
|
|
748
|
-
const validTypes = ["public", "invited", "owner", "admin", "roles", "roles_reverse"];
|
|
749
|
-
if (!validTypes.includes(body.accessType)) {
|
|
750
|
-
return this.errorResponse("Invalid accessType", 400, "VALIDATION_ERROR");
|
|
751
|
-
}
|
|
752
|
-
if (body.accessType === "roles" || body.accessType === "roles_reverse") {
|
|
753
|
-
if (!body.roles || body.roles.length === 0) {
|
|
754
|
-
return this.errorResponse("Roles required for this access type", 400, "VALIDATION_ERROR");
|
|
755
|
-
}
|
|
756
|
-
const validRoles = ["owner", "admin", "member", "guest"];
|
|
757
|
-
if (body.roles.some((r) => !validRoles.includes(r))) {
|
|
758
|
-
return this.errorResponse("Invalid role name", 400, "VALIDATION_ERROR");
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
const policy = await this.store.updateAccessPolicy(id, {
|
|
763
|
-
name: body.name,
|
|
764
|
-
description: body.description?.slice(0, 256),
|
|
765
|
-
accessType: body.accessType,
|
|
766
|
-
roles: body.roles,
|
|
767
|
-
});
|
|
768
|
-
await this.store.createAuditLog({
|
|
769
|
-
action: "access_policy.update",
|
|
770
|
-
operatorDid: caller.did,
|
|
771
|
-
metadata: { policyId: id },
|
|
772
|
-
ip: caller.ip,
|
|
773
|
-
instanceDid,
|
|
774
|
-
});
|
|
775
|
-
return this.jsonResponse({ ok: true, policy });
|
|
776
|
-
}
|
|
777
|
-
async handleDeleteAccessPolicy(caller, id, instanceDid) {
|
|
778
|
-
requirePermission(caller.role, "access_policy.delete");
|
|
779
|
-
const existing = await this.store.getAccessPolicy(id);
|
|
780
|
-
if (!existing) {
|
|
781
|
-
return this.errorResponse("Policy not found", 404, "NOT_FOUND");
|
|
782
|
-
}
|
|
783
|
-
if (existing.isProtected) {
|
|
784
|
-
return this.errorResponse("Cannot delete protected policies", 403, "FORBIDDEN");
|
|
785
|
-
}
|
|
786
|
-
// Instance context: can only delete own instance's policies
|
|
787
|
-
if (instanceDid && existing.instanceDid === null) {
|
|
788
|
-
return this.errorResponse("Cannot delete global policies from instance context", 403, "FORBIDDEN");
|
|
789
|
-
}
|
|
790
|
-
if (instanceDid && existing.instanceDid !== null && existing.instanceDid !== instanceDid) {
|
|
791
|
-
return this.errorResponse("Policy not found", 404, "NOT_FOUND");
|
|
792
|
-
}
|
|
793
|
-
const ruleCount = await this.store.getAccessPolicyRuleCount(id);
|
|
794
|
-
if (ruleCount > 0) {
|
|
795
|
-
return this.errorResponse("Cannot delete policy referenced by security rules", 409, "CONFLICT");
|
|
796
|
-
}
|
|
797
|
-
await this.store.deleteAccessPolicy(id);
|
|
798
|
-
await this.store.createAuditLog({
|
|
799
|
-
action: "access_policy.delete",
|
|
800
|
-
operatorDid: caller.did,
|
|
801
|
-
metadata: { policyId: id, name: existing.name },
|
|
802
|
-
ip: caller.ip,
|
|
803
|
-
instanceDid,
|
|
804
|
-
});
|
|
805
|
-
return this.jsonResponse({ ok: true });
|
|
806
|
-
}
|
|
807
|
-
// ─── Security rule handlers ────────────────────────────────────────
|
|
808
|
-
async handleListSecurityRules(caller, instanceDid) {
|
|
809
|
-
requirePermission(caller.role, "security_rule.list");
|
|
810
|
-
const rules = await this.store.getSecurityRules(instanceDid);
|
|
811
|
-
return this.jsonResponse({ ok: true, rules });
|
|
812
|
-
}
|
|
813
|
-
async handleCreateSecurityRule(caller, request, instanceDid) {
|
|
814
|
-
requirePermission(caller.role, "security_rule.create");
|
|
815
|
-
const body = await this.parseJSON(request);
|
|
816
|
-
if (!body.pathPattern) {
|
|
817
|
-
return this.errorResponse("pathPattern is required", 400, "VALIDATION_ERROR");
|
|
818
|
-
}
|
|
819
|
-
if (!body.accessPolicyId) {
|
|
820
|
-
return this.errorResponse("accessPolicyId is required", 400, "VALIDATION_ERROR");
|
|
821
|
-
}
|
|
822
|
-
// Validate policy exists
|
|
823
|
-
const policy = await this.store.getAccessPolicy(body.accessPolicyId);
|
|
824
|
-
if (!policy) {
|
|
825
|
-
return this.errorResponse("Access policy not found", 400, "VALIDATION_ERROR");
|
|
826
|
-
}
|
|
827
|
-
// Priority must be >= 0
|
|
828
|
-
if (body.priority !== undefined && body.priority < 0) {
|
|
829
|
-
return this.errorResponse("Priority must be >= 0", 400, "VALIDATION_ERROR");
|
|
830
|
-
}
|
|
831
|
-
const rule = await this.store.createSecurityRule({
|
|
832
|
-
pathPattern: body.pathPattern,
|
|
833
|
-
accessPolicyId: body.accessPolicyId,
|
|
834
|
-
priority: body.priority,
|
|
835
|
-
remark: body.remark,
|
|
836
|
-
instanceDid,
|
|
837
|
-
});
|
|
838
|
-
await this.store.createAuditLog({
|
|
839
|
-
action: "security_rule.create",
|
|
840
|
-
operatorDid: caller.did,
|
|
841
|
-
metadata: { ruleId: rule.id, pathPattern: body.pathPattern },
|
|
842
|
-
ip: caller.ip,
|
|
843
|
-
instanceDid,
|
|
844
|
-
});
|
|
845
|
-
return this.jsonResponse({ ok: true, rule });
|
|
846
|
-
}
|
|
847
|
-
async handleUpdateSecurityRule(caller, id, request, instanceDid) {
|
|
848
|
-
requirePermission(caller.role, "security_rule.update");
|
|
849
|
-
const existing = await this.store.getSecurityRule(id);
|
|
850
|
-
if (!existing) {
|
|
851
|
-
return this.errorResponse("Rule not found", 404, "NOT_FOUND");
|
|
852
|
-
}
|
|
853
|
-
// Instance context: can only modify own instance's rules, not global
|
|
854
|
-
if (instanceDid && existing.instanceDid === null) {
|
|
855
|
-
return this.errorResponse("Cannot modify global rules from instance context", 403, "FORBIDDEN");
|
|
856
|
-
}
|
|
857
|
-
if (instanceDid && existing.instanceDid !== null && existing.instanceDid !== instanceDid) {
|
|
858
|
-
return this.errorResponse("Rule not found", 404, "NOT_FOUND");
|
|
859
|
-
}
|
|
860
|
-
const body = await this.parseJSON(request);
|
|
861
|
-
// Default rule constraints
|
|
862
|
-
if (id === "default") {
|
|
863
|
-
if (body.pathPattern !== undefined) {
|
|
864
|
-
return this.errorResponse("Cannot change default rule pattern", 400, "VALIDATION_ERROR");
|
|
865
|
-
}
|
|
866
|
-
if (body.priority !== undefined) {
|
|
867
|
-
return this.errorResponse("Cannot change default rule priority", 400, "VALIDATION_ERROR");
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
// Validate policy exists if changing it
|
|
871
|
-
if (body.accessPolicyId !== undefined) {
|
|
872
|
-
const policy = await this.store.getAccessPolicy(body.accessPolicyId);
|
|
873
|
-
if (!policy) {
|
|
874
|
-
return this.errorResponse("Access policy not found", 400, "VALIDATION_ERROR");
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
if (body.priority !== undefined && body.priority < 0) {
|
|
878
|
-
return this.errorResponse("Priority must be >= 0", 400, "VALIDATION_ERROR");
|
|
879
|
-
}
|
|
880
|
-
const rule = await this.store.updateSecurityRule(id, {
|
|
881
|
-
pathPattern: body.pathPattern,
|
|
882
|
-
accessPolicyId: body.accessPolicyId,
|
|
883
|
-
priority: body.priority,
|
|
884
|
-
enabled: body.enabled,
|
|
885
|
-
remark: body.remark,
|
|
886
|
-
});
|
|
887
|
-
await this.store.createAuditLog({
|
|
888
|
-
action: "security_rule.update",
|
|
889
|
-
operatorDid: caller.did,
|
|
890
|
-
metadata: { ruleId: id },
|
|
891
|
-
ip: caller.ip,
|
|
892
|
-
instanceDid,
|
|
893
|
-
});
|
|
894
|
-
return this.jsonResponse({ ok: true, rule });
|
|
895
|
-
}
|
|
896
|
-
async handleDeleteSecurityRule(caller, id, instanceDid) {
|
|
897
|
-
requirePermission(caller.role, "security_rule.delete");
|
|
898
|
-
if (id === "default") {
|
|
899
|
-
return this.errorResponse("Cannot delete the default rule", 403, "FORBIDDEN");
|
|
900
|
-
}
|
|
901
|
-
const existing = await this.store.getSecurityRule(id);
|
|
902
|
-
if (!existing) {
|
|
903
|
-
return this.errorResponse("Rule not found", 404, "NOT_FOUND");
|
|
904
|
-
}
|
|
905
|
-
// Instance context: can only delete own instance's rules, not global
|
|
906
|
-
if (instanceDid && existing.instanceDid === null) {
|
|
907
|
-
return this.errorResponse("Cannot delete global rules from instance context", 403, "FORBIDDEN");
|
|
908
|
-
}
|
|
909
|
-
if (instanceDid && existing.instanceDid !== null && existing.instanceDid !== instanceDid) {
|
|
910
|
-
return this.errorResponse("Rule not found", 404, "NOT_FOUND");
|
|
911
|
-
}
|
|
912
|
-
await this.store.deleteSecurityRule(id);
|
|
913
|
-
await this.store.createAuditLog({
|
|
914
|
-
action: "security_rule.delete",
|
|
915
|
-
operatorDid: caller.did,
|
|
916
|
-
metadata: { ruleId: id, pathPattern: existing.pathPattern },
|
|
917
|
-
ip: caller.ip,
|
|
918
|
-
instanceDid,
|
|
919
|
-
});
|
|
920
|
-
return this.jsonResponse({ ok: true });
|
|
921
|
-
}
|
|
922
|
-
// ─── Settings handlers ─────────────────────────────────────────────
|
|
923
|
-
/** Resolve the effective instanceDid for settings operations. */
|
|
924
|
-
get settingsInstanceDid() {
|
|
925
|
-
return this.instanceDid ?? "_global_";
|
|
926
|
-
}
|
|
927
|
-
/** Mask a sensitive value: show last 4 chars, prefix with ***. Values ≤4 chars → just ***. */
|
|
928
|
-
maskSecret(value) {
|
|
929
|
-
if (value == null)
|
|
930
|
-
return undefined;
|
|
931
|
-
if (value.length <= 4)
|
|
932
|
-
return "***";
|
|
933
|
-
return `***${value.slice(-4)}`;
|
|
934
|
-
}
|
|
935
|
-
/** Valid OAuth provider names (aligned with ADAPTERS constant). */
|
|
936
|
-
static VALID_OAUTH_PROVIDERS = [
|
|
937
|
-
"google", "github", "apple", "twitter", "facebook", "auth0", "auth0-legacy",
|
|
938
|
-
];
|
|
939
|
-
/** Check if a value is a masked placeholder that should preserve the old value. */
|
|
940
|
-
isMaskedValue(value) {
|
|
941
|
-
return typeof value === "string" && /^\*{3}/.test(value);
|
|
942
|
-
}
|
|
943
|
-
/** Sanitize object keys to prevent prototype pollution. */
|
|
944
|
-
sanitizeKeys(obj) {
|
|
945
|
-
const dangerous = ["__proto__", "constructor", "prototype"];
|
|
946
|
-
const result = {};
|
|
947
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
948
|
-
if (!dangerous.includes(key)) {
|
|
949
|
-
result[key] = value;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return result;
|
|
953
|
-
}
|
|
954
|
-
async handleGetSettings(caller) {
|
|
955
|
-
requirePermission(caller.role, "settings.view");
|
|
956
|
-
const did = this.settingsInstanceDid;
|
|
957
|
-
const allSettings = await this.store.listSettings(did);
|
|
958
|
-
// Aggregate settings by category
|
|
959
|
-
const oauthProviders = {};
|
|
960
|
-
let builtinProviders = {};
|
|
961
|
-
let session = {};
|
|
962
|
-
let email = {};
|
|
963
|
-
for (const s of allSettings) {
|
|
964
|
-
if (!s.value)
|
|
965
|
-
continue;
|
|
966
|
-
try {
|
|
967
|
-
if (s.key.startsWith("oauth:")) {
|
|
968
|
-
const provider = s.key.slice(6);
|
|
969
|
-
const parsed = JSON.parse(s.value);
|
|
970
|
-
// Mask sensitive fields
|
|
971
|
-
if (parsed.clientSecret)
|
|
972
|
-
parsed.clientSecret = this.maskSecret(parsed.clientSecret);
|
|
973
|
-
if (parsed.privateKey)
|
|
974
|
-
parsed.privateKey = this.maskSecret(parsed.privateKey);
|
|
975
|
-
// Apply defaults for management fields
|
|
976
|
-
parsed.enabled = parsed.enabled ?? true;
|
|
977
|
-
parsed.order = parsed.order ?? 999;
|
|
978
|
-
oauthProviders[provider] = parsed;
|
|
979
|
-
}
|
|
980
|
-
else if (s.key === "auth:builtin-providers") {
|
|
981
|
-
builtinProviders = JSON.parse(s.value);
|
|
982
|
-
}
|
|
983
|
-
else if (s.key === "session:config") {
|
|
984
|
-
session = JSON.parse(s.value);
|
|
985
|
-
}
|
|
986
|
-
else if (s.key === "email:config") {
|
|
987
|
-
const parsed = JSON.parse(s.value);
|
|
988
|
-
if (parsed.resendApiKey)
|
|
989
|
-
parsed.resendApiKey = this.maskSecret(parsed.resendApiKey);
|
|
990
|
-
email = parsed;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
catch {
|
|
994
|
-
// Skip invalid JSON
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return this.jsonResponse({
|
|
998
|
-
ok: true,
|
|
999
|
-
settings: { oauthProviders, builtinProviders, session, email },
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
async handleSaveOAuthProvider(caller, provider, request) {
|
|
1003
|
-
requirePermission(caller.role, "settings.edit");
|
|
1004
|
-
if (!TeamHandler.VALID_OAUTH_PROVIDERS.includes(provider)) {
|
|
1005
|
-
return this.errorResponse(`Invalid provider: ${provider}`, 400, "VALIDATION_ERROR");
|
|
1006
|
-
}
|
|
1007
|
-
const body = this.sanitizeKeys(await this.parseJSON(request));
|
|
1008
|
-
// Validate required fields based on provider
|
|
1009
|
-
if (provider === "apple") {
|
|
1010
|
-
if (!body.teamId || !body.keyId || !body.serviceId) {
|
|
1011
|
-
return this.errorResponse("Apple requires teamId, keyId, and serviceId", 400, "VALIDATION_ERROR");
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
else if (provider === "auth0" || provider === "auth0-legacy") {
|
|
1015
|
-
if (!body.clientId || !body.domain) {
|
|
1016
|
-
return this.errorResponse("Auth0 requires clientId and domain", 400, "VALIDATION_ERROR");
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
if (!body.clientId) {
|
|
1021
|
-
return this.errorResponse("clientId is required", 400, "VALIDATION_ERROR");
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
// If secret fields are masked, preserve old values
|
|
1025
|
-
const did = this.settingsInstanceDid;
|
|
1026
|
-
const settingKey = `oauth:${provider}`;
|
|
1027
|
-
if (this.isMaskedValue(body.clientSecret) || this.isMaskedValue(body.privateKey)) {
|
|
1028
|
-
const oldRaw = await this.store.getSetting(did, settingKey);
|
|
1029
|
-
if (oldRaw) {
|
|
1030
|
-
try {
|
|
1031
|
-
const old = JSON.parse(oldRaw);
|
|
1032
|
-
if (this.isMaskedValue(body.clientSecret) && old.clientSecret) {
|
|
1033
|
-
body.clientSecret = old.clientSecret;
|
|
1034
|
-
}
|
|
1035
|
-
if (this.isMaskedValue(body.privateKey) && old.privateKey) {
|
|
1036
|
-
body.privateKey = old.privateKey;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
catch {
|
|
1040
|
-
// ignore
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
await this.store.setSetting(did, settingKey, JSON.stringify(body));
|
|
1045
|
-
await this.store.createAuditLog({
|
|
1046
|
-
action: "settings.oauth_update",
|
|
1047
|
-
operatorDid: caller.did,
|
|
1048
|
-
metadata: { provider, action: "save" },
|
|
1049
|
-
ip: caller.ip,
|
|
1050
|
-
});
|
|
1051
|
-
return this.jsonResponse({ ok: true });
|
|
1052
|
-
}
|
|
1053
|
-
async handleDeleteOAuthProvider(caller, provider) {
|
|
1054
|
-
requirePermission(caller.role, "settings.edit");
|
|
1055
|
-
const did = this.settingsInstanceDid;
|
|
1056
|
-
const settingKey = `oauth:${provider}`;
|
|
1057
|
-
const existing = await this.store.getSetting(did, settingKey);
|
|
1058
|
-
if (!existing) {
|
|
1059
|
-
return this.errorResponse("Provider not found", 404, "NOT_FOUND");
|
|
1060
|
-
}
|
|
1061
|
-
await this.store.deleteSetting(did, settingKey);
|
|
1062
|
-
await this.store.createAuditLog({
|
|
1063
|
-
action: "settings.oauth_delete",
|
|
1064
|
-
operatorDid: caller.did,
|
|
1065
|
-
metadata: { provider },
|
|
1066
|
-
ip: caller.ip,
|
|
1067
|
-
});
|
|
1068
|
-
return this.jsonResponse({ ok: true });
|
|
1069
|
-
}
|
|
1070
|
-
async handleSaveBuiltinProviders(caller, request) {
|
|
1071
|
-
requirePermission(caller.role, "settings.edit");
|
|
1072
|
-
const body = this.sanitizeKeys(await this.parseJSON(request));
|
|
1073
|
-
// Validate structure: each provider must have { enabled: boolean }
|
|
1074
|
-
for (const key of ["passkey", "email", "wallet"]) {
|
|
1075
|
-
const entry = body[key];
|
|
1076
|
-
if (entry && typeof entry.enabled !== "boolean") {
|
|
1077
|
-
return this.errorResponse(`${key}.enabled must be a boolean`, 400, "VALIDATION_ERROR");
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
const did = this.settingsInstanceDid;
|
|
1081
|
-
await this.store.setSetting(did, "auth:builtin-providers", JSON.stringify(body));
|
|
1082
|
-
await this.store.createAuditLog({
|
|
1083
|
-
action: "settings.builtin_providers_update",
|
|
1084
|
-
operatorDid: caller.did,
|
|
1085
|
-
metadata: { providers: body },
|
|
1086
|
-
ip: caller.ip,
|
|
1087
|
-
});
|
|
1088
|
-
return this.jsonResponse({ ok: true });
|
|
1089
|
-
}
|
|
1090
|
-
async handleSaveSessionConfig(caller, request) {
|
|
1091
|
-
requirePermission(caller.role, "settings.edit");
|
|
1092
|
-
const body = await this.parseJSON(request);
|
|
1093
|
-
const ttl = body.jwtTtlDays;
|
|
1094
|
-
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1 || ttl > 30) {
|
|
1095
|
-
return this.errorResponse("jwtTtlDays must be an integer between 1 and 30", 400, "VALIDATION_ERROR");
|
|
1096
|
-
}
|
|
1097
|
-
const did = this.settingsInstanceDid;
|
|
1098
|
-
await this.store.setSetting(did, "session:config", JSON.stringify({ jwtTtlDays: ttl }));
|
|
1099
|
-
await this.store.createAuditLog({
|
|
1100
|
-
action: "settings.session_update",
|
|
1101
|
-
operatorDid: caller.did,
|
|
1102
|
-
metadata: { jwtTtlDays: ttl },
|
|
1103
|
-
ip: caller.ip,
|
|
1104
|
-
});
|
|
1105
|
-
return this.jsonResponse({ ok: true });
|
|
1106
|
-
}
|
|
1107
|
-
async handleSaveEmailConfig(caller, request) {
|
|
1108
|
-
requirePermission(caller.role, "settings.edit");
|
|
1109
|
-
const body = this.sanitizeKeys(await this.parseJSON(request));
|
|
1110
|
-
const { resendApiKey, fromAddress } = body;
|
|
1111
|
-
// Validate email format
|
|
1112
|
-
if (!fromAddress || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fromAddress)) {
|
|
1113
|
-
return this.errorResponse("Invalid email address", 400, "VALIDATION_ERROR");
|
|
1114
|
-
}
|
|
1115
|
-
// If API key is masked, preserve old value
|
|
1116
|
-
const did = this.settingsInstanceDid;
|
|
1117
|
-
let finalApiKey = resendApiKey;
|
|
1118
|
-
if (this.isMaskedValue(resendApiKey)) {
|
|
1119
|
-
const oldRaw = await this.store.getSetting(did, "email:config");
|
|
1120
|
-
if (oldRaw) {
|
|
1121
|
-
try {
|
|
1122
|
-
const old = JSON.parse(oldRaw);
|
|
1123
|
-
if (old.resendApiKey)
|
|
1124
|
-
finalApiKey = old.resendApiKey;
|
|
1125
|
-
}
|
|
1126
|
-
catch {
|
|
1127
|
-
// ignore
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
if (!finalApiKey) {
|
|
1132
|
-
return this.errorResponse("resendApiKey is required", 400, "VALIDATION_ERROR");
|
|
1133
|
-
}
|
|
1134
|
-
await this.store.setSetting(did, "email:config", JSON.stringify({ resendApiKey: finalApiKey, fromAddress }));
|
|
1135
|
-
await this.store.createAuditLog({
|
|
1136
|
-
action: "settings.email_update",
|
|
1137
|
-
operatorDid: caller.did,
|
|
1138
|
-
metadata: { fromAddress },
|
|
1139
|
-
ip: caller.ip,
|
|
1140
|
-
});
|
|
1141
|
-
return this.jsonResponse({ ok: true });
|
|
1142
|
-
}
|
|
1143
|
-
// ─── Page handlers ─────────────────────────────────────────────────
|
|
1144
|
-
async serveAdminPage(request, instanceDid) {
|
|
1145
|
-
// Verify JWT — if not authenticated, return login page
|
|
1146
|
-
const caller = await this.passkey.verify(request);
|
|
1147
|
-
if (!caller) {
|
|
1148
|
-
return await this.passkey.getLoginPage(this.instanceDid);
|
|
1149
|
-
}
|
|
1150
|
-
// Look up user to get role
|
|
1151
|
-
const user = await this.store.getUserByDid(caller.did);
|
|
1152
|
-
if (!user || !user.approved) {
|
|
1153
|
-
return await this.passkey.getLoginPage(this.instanceDid);
|
|
1154
|
-
}
|
|
1155
|
-
// Resolve effective role for instance context
|
|
1156
|
-
let effectiveRole = user.role ?? "member";
|
|
1157
|
-
if (instanceDid) {
|
|
1158
|
-
const resolved = await resolveInstanceRole(this.store, caller.did, instanceDid, user.role ?? undefined);
|
|
1159
|
-
if (!resolved) {
|
|
1160
|
-
return await this.passkey.getLoginPage(this.instanceDid);
|
|
1161
|
-
}
|
|
1162
|
-
effectiveRole = resolved;
|
|
1163
|
-
}
|
|
1164
|
-
const html = buildAdminPageHTML({
|
|
1165
|
-
caller: {
|
|
1166
|
-
did: caller.did,
|
|
1167
|
-
fullName: user.fullName ?? undefined,
|
|
1168
|
-
role: effectiveRole,
|
|
1169
|
-
avatar: user.avatar ?? undefined,
|
|
1170
|
-
},
|
|
1171
|
-
basePath: this.apiBase,
|
|
1172
|
-
instanceDid,
|
|
1173
|
-
});
|
|
1174
|
-
return new Response(html, {
|
|
1175
|
-
headers: {
|
|
1176
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1177
|
-
"Cache-Control": "private, no-store",
|
|
1178
|
-
},
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
serveInvitePage() {
|
|
1182
|
-
const html = buildInvitePageHTML(this.apiBase);
|
|
1183
|
-
return new Response(html, {
|
|
1184
|
-
headers: {
|
|
1185
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1186
|
-
"Cache-Control": "private, no-store",
|
|
1187
|
-
},
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
1191
|
-
jsonResponse(data, status = 200) {
|
|
1192
|
-
return new Response(JSON.stringify(data), {
|
|
1193
|
-
status,
|
|
1194
|
-
headers: {
|
|
1195
|
-
"Content-Type": "application/json",
|
|
1196
|
-
"Cache-Control": "private, no-store",
|
|
1197
|
-
},
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
errorResponse(message, status, code) {
|
|
1201
|
-
return this.jsonResponse({ ok: false, error: message, code }, status);
|
|
1202
|
-
}
|
|
1203
|
-
async parseJSON(request) {
|
|
1204
|
-
try {
|
|
1205
|
-
return (await request.json());
|
|
1206
|
-
}
|
|
1207
|
-
catch {
|
|
1208
|
-
throw new AuthError("Invalid JSON body", 400, "VALIDATION_ERROR");
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
class AuthError extends Error {
|
|
1213
|
-
status;
|
|
1214
|
-
code;
|
|
1215
|
-
constructor(message, status, code) {
|
|
1216
|
-
super(message);
|
|
1217
|
-
this.status = status;
|
|
1218
|
-
this.code = code;
|
|
1219
|
-
this.name = "AuthError";
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
export function createTeamHandler(options) {
|
|
1223
|
-
return new TeamHandler(options);
|
|
1224
|
-
}
|
|
1225
|
-
//# sourceMappingURL=team-handler.js.map
|