@plutocms/supabase 0.15.0 → 0.15.1

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.
Files changed (50) hide show
  1. package/.prettierignore +6 -0
  2. package/CHANGELOG.md +7 -0
  3. package/app/components/access/DeleteRoleModal.vue +17 -4
  4. package/app/components/access/EditRolesSlideover.vue +11 -5
  5. package/app/components/access/InviteModal.vue +44 -19
  6. package/app/components/access/LinkField.vue +14 -3
  7. package/app/components/access/LinkModal.vue +5 -1
  8. package/app/components/access/RevokeModal.vue +17 -4
  9. package/app/components/access/RoleSlideover.vue +32 -11
  10. package/app/components/mail/AuthEmailsPanel.vue +87 -20
  11. package/app/components/migrations/MigrationsBanner.vue +5 -4
  12. package/app/composables/migrations.ts +13 -6
  13. package/app/pages/admin/(auth)/accept-invite.vue +21 -5
  14. package/app/pages/admin/(auth)/forgot-password.vue +11 -4
  15. package/app/pages/admin/(auth)/signup.vue +10 -9
  16. package/app/pages/admin/(auth)/update-password.vue +8 -3
  17. package/app/pages/admin/migrations.vue +8 -3
  18. package/app/pages/admin/settings/roles.vue +58 -15
  19. package/app/pages/admin/settings/users.vue +105 -30
  20. package/app/pages/admin/setup.vue +26 -7
  21. package/app/plugins/pluto-extension.ts +21 -8
  22. package/app/utils/access-ui.ts +29 -8
  23. package/app/utils/api-failure.ts +3 -1
  24. package/app/utils/auth-driver.ts +1 -3
  25. package/app/utils/auth-error.ts +1 -1
  26. package/app/utils/invite-link.ts +3 -3
  27. package/app/utils/supabase-project.ts +8 -3
  28. package/modules/pluto-migrations.ts +14 -3
  29. package/package.json +4 -2
  30. package/server/api/_pluto/supabase/access/users/[id]/resend.post.ts +15 -5
  31. package/server/api/_pluto/supabase/access/users/[id]/roles.put.ts +4 -1
  32. package/server/api/_pluto/supabase/access/users/invite-link.post.ts +12 -3
  33. package/server/api/_pluto/supabase/access/users/invite.post.ts +7 -2
  34. package/server/api/_pluto/supabase/auth/signup.post.ts +5 -1
  35. package/server/api/_pluto/supabase/me/capabilities.get.ts +4 -1
  36. package/server/api/_pluto/supabase/migrations/run.post.ts +9 -2
  37. package/server/api/_pluto/supabase/setup/index.post.ts +4 -1
  38. package/server/utils/access-errors.ts +2 -1
  39. package/server/utils/access-invite.ts +1 -1
  40. package/server/utils/access-mail.ts +72 -28
  41. package/server/utils/capability-guard.ts +5 -2
  42. package/server/utils/content-adapter.ts +75 -24
  43. package/server/utils/migrations.ts +0 -0
  44. package/server/utils/pending-migrations.ts +0 -0
  45. package/server/utils/service-client.ts +3 -1
  46. package/server/utils/sign-in-link.ts +25 -9
  47. package/server/utils/typegen.ts +6 -4
  48. package/server/utils/validate.ts +4 -1
  49. package/shared/types/supabase.ts +62 -65
  50. package/tailwind.lint.css +9 -0
@@ -21,8 +21,13 @@ function inviteFailed() {
21
21
  }
22
22
 
23
23
  /** Finds the id of the account with this email, or null. */
24
- export async function findUserIdByEmail(client: UserClient, email: string): Promise<string | null> {
25
- const { data, error } = await client.rpc('find_user_id_by_email', { p_email: email })
24
+ export async function findUserIdByEmail(
25
+ client: UserClient,
26
+ email: string
27
+ ): Promise<string | null> {
28
+ const { data, error } = await client.rpc('find_user_id_by_email', {
29
+ p_email: email,
30
+ })
26
31
 
27
32
  if (error) {
28
33
  throw mapAccessError(error)
@@ -32,8 +37,15 @@ export async function findUserIdByEmail(client: UserClient, email: string): Prom
32
37
  }
33
38
 
34
39
  /** Adds roles to a user. It keeps the roles the user has. */
35
- export async function addRoles(client: UserClient, target: string, roleKeys: string[]): Promise<void> {
36
- const { error } = await client.rpc('add_user_roles', { target, role_keys: roleKeys })
40
+ export async function addRoles(
41
+ client: UserClient,
42
+ target: string,
43
+ roleKeys: string[]
44
+ ): Promise<void> {
45
+ const { error } = await client.rpc('add_user_roles', {
46
+ target,
47
+ role_keys: roleKeys,
48
+ })
37
49
 
38
50
  if (error) {
39
51
  throw mapAccessError(error)
@@ -44,7 +56,11 @@ export async function addRoles(client: UserClient, target: string, roleKeys: str
44
56
  * The URL that the user opens to accept an invite. The accept-invite page
45
57
  * reads `token_hash` and `type`, then calls `verifyOtp`.
46
58
  */
47
- export function acceptInviteUrl(origin: string, hashedToken: string, type: AcceptLinkType): string {
59
+ export function acceptInviteUrl(
60
+ origin: string,
61
+ hashedToken: string,
62
+ type: AcceptLinkType
63
+ ): string {
48
64
  const url = new URL(INVITE_ACCEPT_PATH, origin)
49
65
 
50
66
  url.searchParams.set('token_hash', hashedToken)
@@ -62,8 +78,7 @@ interface GenerateLinkParams {
62
78
  }
63
79
 
64
80
  type GeneratedLink =
65
- | { kind: 'link', userId: string, link: string }
66
- | { kind: 'exists' }
81
+ { kind: 'link'; userId: string; link: string } | { kind: 'exists' }
67
82
 
68
83
  /**
69
84
  * Makes a one-time link with the service role key. GoTrue sends no email and
@@ -78,17 +93,23 @@ export async function generateAcceptLink(
78
93
  params: GenerateLinkParams
79
94
  ): Promise<GeneratedLink> {
80
95
  const options = { redirectTo: params.redirectTo }
81
- const { data, error } = params.type === 'invite'
82
- ? await service.auth.admin.generateLink({
83
- type: 'invite',
84
- email: params.email,
85
- options: { ...options, data: params.displayName ? { display_name: params.displayName } : {} },
86
- })
87
- : await service.auth.admin.generateLink({
88
- type: 'magiclink',
89
- email: params.email,
90
- options,
91
- })
96
+ const { data, error } =
97
+ params.type === 'invite'
98
+ ? await service.auth.admin.generateLink({
99
+ type: 'invite',
100
+ email: params.email,
101
+ options: {
102
+ ...options,
103
+ data: params.displayName
104
+ ? { display_name: params.displayName }
105
+ : {},
106
+ },
107
+ })
108
+ : await service.auth.admin.generateLink({
109
+ type: 'magiclink',
110
+ email: params.email,
111
+ options,
112
+ })
92
113
 
93
114
  if (error) {
94
115
  if (error.code === 'email_exists' || error.code === 'user_already_exists') {
@@ -118,7 +139,11 @@ export async function generateAcceptLink(
118
139
  ? (verification as AcceptLinkType)
119
140
  : params.type
120
141
 
121
- return { kind: 'link', userId, link: acceptInviteUrl(params.origin, hashedToken, type) }
142
+ return {
143
+ kind: 'link',
144
+ userId,
145
+ link: acceptInviteUrl(params.origin, hashedToken, type),
146
+ }
122
147
  }
123
148
 
124
149
  interface ProvisionParams {
@@ -160,7 +185,9 @@ export function serviceKeyRequiredError() {
160
185
  * If the roles fail, the new user is deleted.
161
186
  * - The account appears after the lookup (a race): the roles are added.
162
187
  */
163
- export async function provisionAccess(params: ProvisionParams): Promise<ProvisionResult> {
188
+ export async function provisionAccess(
189
+ params: ProvisionParams
190
+ ): Promise<ProvisionResult> {
164
191
  const { client, service, email, roleKeys } = params
165
192
  const link = (type: 'invite' | 'magiclink') =>
166
193
  generateAcceptLink(service, { ...params, type })
@@ -177,7 +204,8 @@ export async function provisionAccess(params: ProvisionParams): Promise<Provisio
177
204
  // admin from taking over a customer's or another admin's account. Only an
178
205
  // account that never confirmed (a pending invite, or a sign-up with no
179
206
  // confirmation) gets a link, because it cannot sign in without one.
180
- const { data: found, error: lookupError } = await service.auth.admin.getUserById(userId)
207
+ const { data: found, error: lookupError } =
208
+ await service.auth.admin.getUserById(userId)
181
209
 
182
210
  if (lookupError || !found.user) {
183
211
  console.error('invite: getUserById failed for an existing account')
@@ -224,7 +252,9 @@ export async function provisionAccess(params: ProvisionParams): Promise<Provisio
224
252
  })
225
253
 
226
254
  if (setError) {
227
- const { error: undoError } = await service.auth.admin.deleteUser(made.userId)
255
+ const { error: undoError } = await service.auth.admin.deleteUser(
256
+ made.userId
257
+ )
228
258
 
229
259
  if (undoError) {
230
260
  console.error('invite: could not delete the new user:', undoError.message)
@@ -266,7 +296,10 @@ export async function inviteSiteName(client: UserClient): Promise<string> {
266
296
  }
267
297
 
268
298
  /** The labels of the roles, in the order of the keys. A key with no label stays as it is. */
269
- async function roleLabels(client: UserClient, roleKeys: string[]): Promise<string[]> {
299
+ async function roleLabels(
300
+ client: UserClient,
301
+ roleKeys: string[]
302
+ ): Promise<string[]> {
270
303
  try {
271
304
  const { data } = await client.rpc('list_roles')
272
305
  const labels = new Map((data ?? []).map((role) => [role.key, role.label]))
@@ -292,7 +325,9 @@ interface InviteMailParams {
292
325
  * fails. The caller keeps the user and the roles, and gives the link to the
293
326
  * admin. The log holds the error code only: no recipient, link or key.
294
327
  */
295
- export async function sendInviteMail(params: InviteMailParams): Promise<boolean> {
328
+ export async function sendInviteMail(
329
+ params: InviteMailParams
330
+ ): Promise<boolean> {
296
331
  try {
297
332
  const [siteName, roles] = await Promise.all([
298
333
  inviteSiteName(params.client),
@@ -314,14 +349,19 @@ export async function sendInviteMail(params: InviteMailParams): Promise<boolean>
314
349
  } catch (error) {
315
350
  const code = (error as { code?: unknown } | null)?.code
316
351
 
317
- console.error('invite: the Pluto mail send failed:', typeof code === 'string' ? code : 'unknown')
352
+ console.error(
353
+ 'invite: the Pluto mail send failed:',
354
+ typeof code === 'string' ? code : 'unknown'
355
+ )
318
356
 
319
357
  return false
320
358
  }
321
359
  }
322
360
 
323
361
  /** The name of the admin who invites, for the email. Empty when unknown. */
324
- export function inviterNameOf(user: { user_metadata?: Record<string, unknown> | null }): string | undefined {
362
+ export function inviterNameOf(user: {
363
+ user_metadata?: Record<string, unknown> | null
364
+ }): string | undefined {
325
365
  const meta = user.user_metadata ?? {}
326
366
 
327
367
  for (const value of [meta.display_name, meta.full_name, meta.name]) {
@@ -337,12 +377,16 @@ export function inviterNameOf(user: { user_metadata?: Record<string, unknown> |
337
377
  * Checks the optional email language against the admin languages. Throws a
338
378
  * 400 for a language the admin does not have.
339
379
  */
340
- export function assertInviteLocale(event: H3Event, locale: string | undefined): void {
380
+ export function assertInviteLocale(
381
+ event: H3Event,
382
+ locale: string | undefined
383
+ ): void {
341
384
  if (locale === undefined) {
342
385
  return
343
386
  }
344
387
 
345
- const adminLocales = useRuntimeConfig(event).public.plutoI18n?.adminLocales ?? []
388
+ const adminLocales =
389
+ useRuntimeConfig(event).public.plutoI18n?.adminLocales ?? []
346
390
 
347
391
  if (!adminLocales.includes(locale)) {
348
392
  throw createPlutoError({
@@ -44,7 +44,9 @@ export async function requireCapability(
44
44
  }
45
45
 
46
46
  const client = await serverSupabaseClient<Database>(event)
47
- const { data, error } = await client.rpc('has_capability', { cap: capability })
47
+ const { data, error } = await client.rpc('has_capability', {
48
+ cap: capability,
49
+ })
48
50
 
49
51
  if (error || data !== true) {
50
52
  // A custom `message` without a `messageKey` has no translation, so it
@@ -56,7 +58,8 @@ export async function requireCapability(
56
58
  throw createPlutoError({
57
59
  statusCode: 403,
58
60
  message:
59
- options?.message ?? `Your account is missing the "${capability}" permission.`,
61
+ options?.message ??
62
+ `Your account is missing the "${capability}" permission.`,
60
63
  key: options?.messageKey ?? 'supabase.errors.missingPermission',
61
64
  params: { capability },
62
65
  })
@@ -40,7 +40,8 @@ async function getClient(event: H3Event): Promise<SupabaseClient> {
40
40
  }
41
41
 
42
42
  /** The anonymous client and the url/key pair it was built from. */
43
- let anonymousClient: { url: string, key: string, client: SupabaseClient } | undefined
43
+ let anonymousClient:
44
+ { url: string; key: string; client: SupabaseClient } | undefined
44
45
 
45
46
  /**
46
47
  * A Supabase client with no user session, for public reads of the public
@@ -51,9 +52,16 @@ let anonymousClient: { url: string, key: string, client: SupabaseClient } | unde
51
52
  * whole process.
52
53
  */
53
54
  function getAnonymousClient(event: H3Event): SupabaseClient {
54
- const { url, key } = useRuntimeConfig(event).public.supabase as { url: string, key: string }
55
+ const { url, key } = useRuntimeConfig(event).public.supabase as {
56
+ url: string
57
+ key: string
58
+ }
55
59
 
56
- if (!anonymousClient || anonymousClient.url !== url || anonymousClient.key !== key) {
60
+ if (
61
+ !anonymousClient ||
62
+ anonymousClient.url !== url ||
63
+ anonymousClient.key !== key
64
+ ) {
57
65
  anonymousClient = {
58
66
  url,
59
67
  key,
@@ -98,7 +106,9 @@ export function resolveTimestampColumn(
98
106
  * `undefined` when status is disabled entirely (`type.status === false`
99
107
  * or unset).
100
108
  */
101
- export function resolveStatusColumn(type: PlutoContentType): string | undefined {
109
+ export function resolveStatusColumn(
110
+ type: PlutoContentType
111
+ ): string | undefined {
102
112
  if (!type.status) {
103
113
  return undefined
104
114
  }
@@ -123,7 +133,9 @@ function resolveSortColumn(type: PlutoContentType, fieldName: string): string {
123
133
  function timestampColumns(type: PlutoContentType): string[] {
124
134
  const { created, updated, published } = timestampKeys(type)
125
135
 
126
- return [created, updated, published].filter((column): column is string => column !== undefined)
136
+ return [created, updated, published].filter(
137
+ (column): column is string => column !== undefined
138
+ )
127
139
  }
128
140
 
129
141
  /**
@@ -131,8 +143,12 @@ function timestampColumns(type: PlutoContentType): string[] {
131
143
  * because a timestamp is not a declared field. A key is missing when its
132
144
  * timestamp is off. Core uses this to sort and to show the public dates.
133
145
  */
134
- function timestampKeys(type: PlutoContentType): { created?: string, updated?: string, published?: string } {
135
- const keys: { created?: string, updated?: string, published?: string } = {}
146
+ function timestampKeys(type: PlutoContentType): {
147
+ created?: string
148
+ updated?: string
149
+ published?: string
150
+ } {
151
+ const keys: { created?: string; updated?: string; published?: string } = {}
136
152
 
137
153
  const created = resolveTimestampColumn(type, 'created')
138
154
  if (created) {
@@ -158,7 +174,10 @@ function timestampKeys(type: PlutoContentType): { created?: string, updated?: st
158
174
  * declares, and a timestamp is not a field. A field with the same name as a
159
175
  * timestamp column keeps its own value.
160
176
  */
161
- function toItem(type: PlutoContentType, row: Record<string, unknown>): PlutoContentItem {
177
+ function toItem(
178
+ type: PlutoContentType,
179
+ row: Record<string, unknown>
180
+ ): PlutoContentItem {
162
181
  const item = mapColumnsToFields(type, row)
163
182
 
164
183
  for (const column of timestampColumns(type)) {
@@ -202,7 +221,11 @@ export function toContentError(error: PostgrestError) {
202
221
  }
203
222
 
204
223
  /** Resolves a field's storage column by name, warning and falling back to the raw name when the field is missing. This signals a misconfigured content type. */
205
- function resolveFieldColumn(type: PlutoContentType, fieldName: string, context: string): string {
224
+ function resolveFieldColumn(
225
+ type: PlutoContentType,
226
+ fieldName: string,
227
+ context: string
228
+ ): string {
206
229
  const field = type.fields.find((candidate) => candidate.name === fieldName)
207
230
 
208
231
  if (field) {
@@ -216,7 +239,10 @@ function resolveFieldColumn(type: PlutoContentType, fieldName: string, context:
216
239
  return fieldName
217
240
  }
218
241
 
219
- async function list(ctx: PlutoContentContext, query: PlutoContentQuery): Promise<PlutoContentListResult> {
242
+ async function list(
243
+ ctx: PlutoContentContext,
244
+ query: PlutoContentQuery
245
+ ): Promise<PlutoContentListResult> {
220
246
  const client = await getClient(ctx.event)
221
247
  const pkColumn = ctx.type.primaryKey ?? 'id'
222
248
 
@@ -224,7 +250,8 @@ async function list(ctx: PlutoContentContext, query: PlutoContentQuery): Promise
224
250
 
225
251
  const statusColumn = resolveStatusColumn(ctx.type)
226
252
  if (statusColumn && query.includeUnpublished !== true) {
227
- const publishedValue = (ctx.type.status && ctx.type.status.publishedValue) || 'published'
253
+ const publishedValue =
254
+ (ctx.type.status && ctx.type.status.publishedValue) || 'published'
228
255
  builder = builder.eq(statusColumn, publishedValue)
229
256
  }
230
257
 
@@ -233,14 +260,20 @@ async function list(ctx: PlutoContentContext, query: PlutoContentQuery): Promise
233
260
  }
234
261
 
235
262
  if (query.search) {
236
- const titleColumn = resolveFieldColumn(ctx.type, ctx.type.titleField, 'titleField')
263
+ const titleColumn = resolveFieldColumn(
264
+ ctx.type,
265
+ ctx.type.titleField,
266
+ 'titleField'
267
+ )
237
268
  builder = builder.ilike(titleColumn, `%${query.search}%`)
238
269
  }
239
270
 
240
271
  const sort = query.sort ?? ctx.type.defaultSort
241
272
  if (sort) {
242
273
  const sortColumn = resolveSortColumn(ctx.type, sort.field)
243
- builder = builder.order(sortColumn, { ascending: sort.direction !== 'desc' })
274
+ builder = builder.order(sortColumn, {
275
+ ascending: sort.direction !== 'desc',
276
+ })
244
277
  } else {
245
278
  const createdColumn = resolveTimestampColumn(ctx.type, 'created')
246
279
  builder = builder.order(createdColumn ?? pkColumn, { ascending: false })
@@ -260,7 +293,9 @@ async function list(ctx: PlutoContentContext, query: PlutoContentQuery): Promise
260
293
  }
261
294
 
262
295
  return {
263
- data: (data ?? []).map((row: Record<string, unknown>) => toItem(ctx.type, row)),
296
+ data: (data ?? []).map((row: Record<string, unknown>) =>
297
+ toItem(ctx.type, row)
298
+ ),
264
299
  total: count ?? undefined,
265
300
  }
266
301
  }
@@ -279,7 +314,10 @@ function isInvalidIdShape(error: PostgrestError): boolean {
279
314
  return error.code === '22P02'
280
315
  }
281
316
 
282
- async function get(ctx: PlutoContentContext, idOrSlug: string | number): Promise<PlutoContentItem | null> {
317
+ async function get(
318
+ ctx: PlutoContentContext,
319
+ idOrSlug: string | number
320
+ ): Promise<PlutoContentItem | null> {
283
321
  const client = await getClient(ctx.event)
284
322
  const pkColumn = ctx.type.primaryKey ?? 'id'
285
323
 
@@ -298,7 +336,11 @@ async function get(ctx: PlutoContentContext, idOrSlug: string | number): Promise
298
336
  }
299
337
 
300
338
  if (ctx.type.slug) {
301
- const slugColumn = resolveFieldColumn(ctx.type, ctx.type.slug.field, 'slug.field')
339
+ const slugColumn = resolveFieldColumn(
340
+ ctx.type,
341
+ ctx.type.slug.field,
342
+ 'slug.field'
343
+ )
302
344
 
303
345
  const bySlug = await client
304
346
  .from(ctx.type.source)
@@ -318,7 +360,10 @@ async function get(ctx: PlutoContentContext, idOrSlug: string | number): Promise
318
360
  return null
319
361
  }
320
362
 
321
- async function create(ctx: PlutoContentContext, values: Record<string, unknown>): Promise<PlutoContentItem> {
363
+ async function create(
364
+ ctx: PlutoContentContext,
365
+ values: Record<string, unknown>
366
+ ): Promise<PlutoContentItem> {
322
367
  const client = await getClient(ctx.event)
323
368
  const row = mapFieldsToColumns(ctx.type, values)
324
369
 
@@ -387,10 +432,10 @@ async function update(
387
432
  // edit route: stamp published-at the first time a row is published, but
388
433
  // never overwrite an already-set value on a later edit.
389
434
  if (
390
- statusColumn
391
- && ctx.type.status
392
- && ctx.type.status.publishedAtColumn
393
- && row[statusColumn] === ctx.type.status.publishedValue
435
+ statusColumn &&
436
+ ctx.type.status &&
437
+ ctx.type.status.publishedAtColumn &&
438
+ row[statusColumn] === ctx.type.status.publishedValue
394
439
  ) {
395
440
  const publishedAtColumn = ctx.type.status.publishedAtColumn
396
441
  // Selects the whole row rather than the one dynamic column name:
@@ -407,7 +452,10 @@ async function update(
407
452
  throw toContentError(fetchError)
408
453
  }
409
454
 
410
- if (existing && (existing as Record<string, unknown>)[publishedAtColumn] == null) {
455
+ if (
456
+ existing &&
457
+ (existing as Record<string, unknown>)[publishedAtColumn] == null
458
+ ) {
411
459
  row[publishedAtColumn] = new Date().toISOString()
412
460
  }
413
461
  }
@@ -426,7 +474,10 @@ async function update(
426
474
  return toItem(ctx.type, data)
427
475
  }
428
476
 
429
- async function remove(ctx: PlutoContentContext, id: string | number): Promise<void> {
477
+ async function remove(
478
+ ctx: PlutoContentContext,
479
+ id: string | number
480
+ ): Promise<void> {
430
481
  const client = await getClient(ctx.event)
431
482
  const pkColumn = ctx.type.primaryKey ?? 'id'
432
483
 
@@ -445,7 +496,7 @@ async function remove(ctx: PlutoContentContext, id: string | number): Promise<vo
445
496
  */
446
497
  async function countCreated(
447
498
  ctx: PlutoContentContext,
448
- range: { from: string, to: string }
499
+ range: { from: string; to: string }
449
500
  ): Promise<number | null> {
450
501
  const createdColumn = resolveTimestampColumn(ctx.type, 'created')
451
502
  if (!createdColumn) {
Binary file
Binary file
@@ -15,4 +15,6 @@ export function optionalServiceClient(event: H3Event) {
15
15
  }
16
16
 
17
17
  /** The client of `optionalServiceClient`, when the key is set. */
18
- export type ServiceClient = NonNullable<ReturnType<typeof optionalServiceClient>>
18
+ export type ServiceClient = NonNullable<
19
+ ReturnType<typeof optionalServiceClient>
20
+ >
@@ -40,7 +40,9 @@ export async function sendSignInLink(
40
40
  options: {
41
41
  shouldCreateUser: options.createUser,
42
42
  emailRedirectTo: options.redirectTo,
43
- data: options.displayName ? { display_name: options.displayName } : undefined,
43
+ data: options.displayName
44
+ ? { display_name: options.displayName }
45
+ : undefined,
44
46
  },
45
47
  })
46
48
 
@@ -52,14 +54,17 @@ export async function sendSignInLink(
52
54
  * SMTP, Supabase sends auth emails only to members of the project's
53
55
  * organization.
54
56
  */
55
- export function isEmailNotAuthorizedError(error: { code?: string } | null | undefined): boolean {
57
+ export function isEmailNotAuthorizedError(
58
+ error: { code?: string } | null | undefined
59
+ ): boolean {
56
60
  return error?.code === 'email_address_not_authorized'
57
61
  }
58
62
 
59
63
  export function emailNotAuthorizedError() {
60
64
  return createPlutoError({
61
65
  statusCode: 400,
62
- message: 'Supabase\'s built-in email sends only to members of your Supabase organization. Set up custom SMTP in Supabase to invite other addresses.',
66
+ message:
67
+ "Supabase's built-in email sends only to members of your Supabase organization. Set up custom SMTP in Supabase to invite other addresses.",
63
68
  key: 'supabase.access.errors.emailNotAuthorized',
64
69
  })
65
70
  }
@@ -69,14 +74,17 @@ export function emailNotAuthorizedError() {
69
74
  * 60 seconds, and a few per hour for the project with the built-in mailer.
70
75
  * This is not the in-memory limit of `rate-limit.ts`.
71
76
  */
72
- export function isEmailRateLimitError(error: { code?: string, status?: number } | null | undefined): boolean {
77
+ export function isEmailRateLimitError(
78
+ error: { code?: string; status?: number } | null | undefined
79
+ ): boolean {
73
80
  return error?.status === 429 || error?.code === 'over_email_send_rate_limit'
74
81
  }
75
82
 
76
83
  export function emailRateLimitError() {
77
84
  return createPlutoError({
78
85
  statusCode: 429,
79
- message: 'Supabase sent too many emails. Wait a minute for the same address, or up to an hour with the built-in mailer. Custom SMTP in Supabase raises this limit.',
86
+ message:
87
+ 'Supabase sent too many emails. Wait a minute for the same address, or up to an hour with the built-in mailer. Custom SMTP in Supabase raises this limit.',
80
88
  key: 'supabase.access.errors.emailRateLimited',
81
89
  })
82
90
  }
@@ -85,20 +93,27 @@ export function emailRateLimitError() {
85
93
  * True when Supabase refuses the address itself, for example an example or
86
94
  * test domain (`example.com`), or an address it cannot deliver to.
87
95
  */
88
- export function isEmailInvalidError(error: { code?: string } | null | undefined): boolean {
96
+ export function isEmailInvalidError(
97
+ error: { code?: string } | null | undefined
98
+ ): boolean {
89
99
  return error?.code === 'email_address_invalid'
90
100
  }
91
101
 
92
102
  export function emailInvalidError() {
93
103
  return createPlutoError({
94
104
  statusCode: 400,
95
- message: 'Supabase does not accept this email address. Use a real address: Supabase refuses example and test domains.',
105
+ message:
106
+ 'Supabase does not accept this email address. Use a real address: Supabase refuses example and test domains.',
96
107
  key: 'supabase.access.errors.emailInvalid',
97
108
  })
98
109
  }
99
110
 
100
111
  /** Maps a GoTrue error of `signInWithOtp` to a Pluto error. */
101
- export function signInLinkError(error: { code?: string, status?: number, message: string }) {
112
+ export function signInLinkError(error: {
113
+ code?: string
114
+ status?: number
115
+ message: string
116
+ }) {
102
117
  if (isEmailNotAuthorizedError(error)) {
103
118
  return emailNotAuthorizedError()
104
119
  }
@@ -114,7 +129,8 @@ export function signInLinkError(error: { code?: string, status?: number, message
114
129
  if (error.code === 'signup_disabled' || error.code === 'otp_disabled') {
115
130
  return createPlutoError({
116
131
  statusCode: 503,
117
- message: 'Public sign-up is off. Set NUXT_SUPABASE_SECRET_KEY on the server to send invites.',
132
+ message:
133
+ 'Public sign-up is off. Set NUXT_SUPABASE_SECRET_KEY on the server to send invites.',
118
134
  key: 'supabase.access.errors.signupDisabled',
119
135
  })
120
136
  }
@@ -15,9 +15,10 @@ const execFileAsync = promisify(execFile)
15
15
  */
16
16
  function resolveSupabaseBin(rootDir: string): string {
17
17
  // On Windows, npm installs `supabase.cmd` and Bun installs `supabase.exe`.
18
- const binNames = process.platform === 'win32'
19
- ? ['supabase.cmd', 'supabase.exe']
20
- : ['supabase']
18
+ const binNames =
19
+ process.platform === 'win32'
20
+ ? ['supabase.cmd', 'supabase.exe']
21
+ : ['supabase']
21
22
  const localBin = binNames
22
23
  .map((name) => join(rootDir, 'node_modules/.bin', name))
23
24
  .find((path) => existsSync(path))
@@ -25,7 +26,8 @@ function resolveSupabaseBin(rootDir: string): string {
25
26
  }
26
27
 
27
28
  // The block, with its leading comment lines. The block has no nested braces.
28
- const INTERNAL_BLOCK = /(?:^[ \t]*\/\/[^\n]*\n)*^[ \t]*__InternalSupabase:\s*\{[^{}]*\}[ \t]*\n/m
29
+ const INTERNAL_BLOCK =
30
+ /(?:^[ \t]*\/\/[^\n]*\n)*^[ \t]*__InternalSupabase:\s*\{[^{}]*\}[ \t]*\n/m
29
31
  // The block itself. The name alone is not enough: the output also has it in
30
32
  // `Omit<Database, '__InternalSupabase'>`.
31
33
  const HAS_BLOCK = /^[ \t]*__InternalSupabase:\s*\{/m
@@ -27,7 +27,10 @@ export interface ValidationError {
27
27
  * A nested field path (for example `address.zip`) is joined with `.` to
28
28
  * build `field`.
29
29
  */
30
- export async function parseBody<T>(event: H3Event, schema: ZodSchema<T>): Promise<T> {
30
+ export async function parseBody<T>(
31
+ event: H3Event,
32
+ schema: ZodSchema<T>
33
+ ): Promise<T> {
31
34
  const body = await readBody(event)
32
35
  const result = schema.safeParse(body)
33
36