spine-framework 0.3.56 → 0.3.57
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.
|
@@ -76,7 +76,7 @@ export type { RequestContext, HandlerFunction, HandlerResult } from './middlewar
|
|
|
76
76
|
* createHandler — wraps a handler function with auth, principal resolution, and audit.
|
|
77
77
|
* Use this when writing Netlify function handlers.
|
|
78
78
|
*/
|
|
79
|
-
export { createHandler, requireUserContext, requireSystemContextWithAudit, json, error as errorResponse, cors } from './middleware'
|
|
79
|
+
export { createHandler, createPublicHandler, requireUserContext, requireSystemContextWithAudit, json, error as errorResponse, cors } from './middleware'
|
|
80
80
|
|
|
81
81
|
// ============================================
|
|
82
82
|
// Identity — Principal model
|
|
@@ -274,6 +274,96 @@ export function createHandler(handler: HandlerFunction) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
// ─── PUBLIC HANDLER FACTORY ────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Wraps a handler function without requiring authentication.
|
|
281
|
+
* Used for public endpoints like check-first-user, check-domain, validate-token.
|
|
282
|
+
*
|
|
283
|
+
* Similar to createHandler but allows anonymous principals.
|
|
284
|
+
*/
|
|
285
|
+
export function createPublicHandler(handler: HandlerFunction) {
|
|
286
|
+
return async (event: any, context: any) => {
|
|
287
|
+
// Detect nested call: if event is already a RequestContext with a principal,
|
|
288
|
+
// skip event parsing and call the raw handler directly
|
|
289
|
+
if (event && event.requestId && event.principal) {
|
|
290
|
+
return handler(event, context)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const requestId = crypto.randomUUID()
|
|
294
|
+
const startTime = Date.now()
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// Parse headers
|
|
298
|
+
const appId = event.headers?.['x-app-id'] || event.headers?.['X-App-Id']
|
|
299
|
+
|
|
300
|
+
// Parse query string parameters
|
|
301
|
+
const queryParams: Record<string, string> = {}
|
|
302
|
+
if (event.queryStringParameters) {
|
|
303
|
+
Object.assign(queryParams, event.queryStringParameters)
|
|
304
|
+
}
|
|
305
|
+
if (!queryParams.method && event.httpMethod) {
|
|
306
|
+
queryParams.method = event.httpMethod
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Resolve principal - may be anonymous for public endpoints
|
|
310
|
+
const principal = await resolvePrincipal(event)
|
|
311
|
+
|
|
312
|
+
// Get RLS-scoped database client based on principal type
|
|
313
|
+
// For anonymous users, this returns adminDb (service role)
|
|
314
|
+
const ctxDb = getPrincipalDb(principal)
|
|
315
|
+
|
|
316
|
+
// Build request context
|
|
317
|
+
const ctx: RequestContext = {
|
|
318
|
+
requestId,
|
|
319
|
+
principal,
|
|
320
|
+
db: ctxDb,
|
|
321
|
+
accountId: principal?.accountId || null,
|
|
322
|
+
appId: appId || null,
|
|
323
|
+
query: queryParams,
|
|
324
|
+
requestPath: event.path || '/',
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Parse body
|
|
328
|
+
let body = null
|
|
329
|
+
if (event.body) {
|
|
330
|
+
try {
|
|
331
|
+
body = JSON.parse(event.body)
|
|
332
|
+
} catch (e) {
|
|
333
|
+
return error('Invalid JSON in request body', 400)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (ctx.query.id) {
|
|
337
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
338
|
+
body = {}
|
|
339
|
+
}
|
|
340
|
+
if (!('id' in body)) {
|
|
341
|
+
body.id = ctx.query.id
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Execute handler
|
|
346
|
+
const result = await handler(ctx, body)
|
|
347
|
+
const durationMs = Date.now() - startTime
|
|
348
|
+
|
|
349
|
+
// Return success response
|
|
350
|
+
return json({
|
|
351
|
+
data: result,
|
|
352
|
+
error: null,
|
|
353
|
+
meta: {
|
|
354
|
+
requestId,
|
|
355
|
+
duration: durationMs
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
} catch (err: any) {
|
|
360
|
+
console.error(`[${requestId}] Public handler error:`, err)
|
|
361
|
+
const status = typeof err.statusCode === 'number' ? err.statusCode : 500
|
|
362
|
+
return error(err.message || 'Internal server error', status)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
277
367
|
// ─── RESPONSE HELPERS ────────────────────────────────────────────────────────
|
|
278
368
|
|
|
279
369
|
/**
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* @seeAlso src/components/shared/InviteManager.tsx (UI)
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
import { createHandler } from './_shared/middleware'
|
|
55
|
+
import { createHandler, createPublicHandler } from './_shared/middleware'
|
|
56
56
|
import { adminDb } from './_shared/db'
|
|
57
57
|
import { emitLog } from './_shared/audit'
|
|
58
58
|
|
|
@@ -239,13 +239,14 @@ export const completeRegistration = createHandler(async (ctx, body) => {
|
|
|
239
239
|
.single()
|
|
240
240
|
|
|
241
241
|
// Resolve member_admin role (fall back to member if not seeded yet)
|
|
242
|
-
const { data:
|
|
242
|
+
const { data: roles } = await adminDb
|
|
243
243
|
.from('roles')
|
|
244
244
|
.select('id')
|
|
245
245
|
.in('slug', ['member_admin', 'member'])
|
|
246
246
|
.order('slug') // member_admin sorts before member
|
|
247
247
|
.limit(1)
|
|
248
|
-
|
|
248
|
+
|
|
249
|
+
const role = roles?.[0]
|
|
249
250
|
|
|
250
251
|
// Create account
|
|
251
252
|
const slug = generateAccountSlug(account_name)
|
|
@@ -285,11 +286,13 @@ export const completeRegistration = createHandler(async (ctx, body) => {
|
|
|
285
286
|
if (!target_account_id) throw new Error('target_account_id is required for join_request')
|
|
286
287
|
|
|
287
288
|
// Resolve member role
|
|
288
|
-
const { data:
|
|
289
|
+
const { data: roles } = await adminDb
|
|
289
290
|
.from('roles')
|
|
290
291
|
.select('id')
|
|
291
292
|
.eq('slug', 'member')
|
|
292
|
-
.
|
|
293
|
+
.limit(1)
|
|
294
|
+
|
|
295
|
+
const role = roles?.[0]
|
|
293
296
|
|
|
294
297
|
// Resolve type_id
|
|
295
298
|
const { data: personType } = await adminDb
|
|
@@ -633,16 +636,16 @@ export const deny = createHandler(async (ctx, body) => {
|
|
|
633
636
|
|
|
634
637
|
// ─── MAIN HANDLER ────────────────────────────────────────────────────────────
|
|
635
638
|
|
|
636
|
-
export const handler =
|
|
639
|
+
export const handler = createPublicHandler(async (ctx, body) => {
|
|
637
640
|
const action = ctx.query?.action
|
|
638
641
|
|
|
639
642
|
switch (action) {
|
|
640
|
-
// Public
|
|
643
|
+
// Public endpoints (no auth required)
|
|
641
644
|
case 'check-first-user': return await checkFirstUser(ctx, body)
|
|
642
645
|
case 'check-domain': return await checkDomain(ctx, body)
|
|
643
646
|
case 'validate-token': return await validateToken(ctx, body)
|
|
644
647
|
case 'complete-registration': return await completeRegistration(ctx, body)
|
|
645
|
-
// Protected
|
|
648
|
+
// Protected endpoints (require auth)
|
|
646
649
|
case 'create-invite': return await createInvite(ctx, body)
|
|
647
650
|
case 'list-invites': return await listInvites(ctx, body)
|
|
648
651
|
case 'revoke-invite': return await revokeInvite(ctx, body)
|