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: role } = await adminDb
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
- .single()
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: role } = await adminDb
289
+ const { data: roles } = await adminDb
289
290
  .from('roles')
290
291
  .select('id')
291
292
  .eq('slug', 'member')
292
- .single()
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 = createHandler(async (ctx, body) => {
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.56",
3
+ "version": "0.3.57",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {