better-supabase 0.5.0 → 0.5.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 (62) hide show
  1. package/README.md +133 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
  5. package/dist/cli/gen-CIdSQZad.js +2 -0
  6. package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
  7. package/dist/cli/index.d.ts +1 -1
  8. package/dist/cli/index.js +3 -3
  9. package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
  10. package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
  11. package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
  12. package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
  13. package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
  14. package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
  15. package/dist/client/index.js +1 -1
  16. package/dist/config/index.d.ts +2 -2
  17. package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
  18. package/dist/edge/index.d.ts +2 -2
  19. package/dist/edge/index.js +2 -2
  20. package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  21. package/dist/hono/index.d.ts +3 -3
  22. package/dist/hono/index.js +2 -2
  23. package/dist/impersonation-BgSHAYLf.js +133 -0
  24. package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
  25. package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
  26. package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
  27. package/dist/mcp/index.d.ts +2 -2
  28. package/dist/mcp/index.js +2 -2
  29. package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
  30. package/dist/next/index.d.ts +6 -6
  31. package/dist/next/index.js +3 -3
  32. package/dist/orpc/index.d.ts +2 -2
  33. package/dist/orpc/index.js +1 -1
  34. package/dist/otel/index.js +5 -5
  35. package/dist/react/index.d.ts +5 -5
  36. package/dist/react/server.d.ts +5 -5
  37. package/dist/react/session.d.ts +2 -2
  38. package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  39. package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
  40. package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
  41. package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
  42. package/dist/server/index.d.ts +9 -8
  43. package/dist/server/index.js +5 -5
  44. package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
  45. package/dist/sql/index.d.ts +38 -3
  46. package/dist/sql/index.js +237 -118
  47. package/dist/ssr/index.d.ts +4 -4
  48. package/dist/ssr/index.js +1 -1
  49. package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
  50. package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
  51. package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
  52. package/dist/testing/index.d.ts +36 -4
  53. package/dist/testing/index.js +141 -1
  54. package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
  55. package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
  56. package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
  57. package/package.json +13 -13
  58. package/schemas/doctor-report-v1.json +1 -0
  59. package/skills/better-supabase-auth/SKILL.md +15 -7
  60. package/skills/better-supabase-auth/references/permdock.md +22 -0
  61. package/dist/cli/gen-BJc9vdyu.js +0 -2
  62. package/dist/impersonation-VfaGc4Q_.js +0 -41
@@ -970,6 +970,146 @@ async function expectTenantIsolation(betterSupabase, options) {
970
970
  }
971
971
  }
972
972
  //#endregion
973
+ //#region src/testing/claims-fixtures.ts
974
+ const base = {
975
+ sub: "6f1c2c1e-5d0a-4d9e-9a51-6b1f0e7c2a10",
976
+ aud: "authenticated",
977
+ role: "authenticated",
978
+ iss: "https://project.supabase.co/auth/v1",
979
+ aal: "aal1",
980
+ session_id: "b7a8f9c0-1111-4222-8333-944455556666",
981
+ is_anonymous: false
982
+ };
983
+ const admin = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d";
984
+ const supportSessionId = "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b";
985
+ const client = "5f0e4d3c-2b1a-4098-8776-655443322110";
986
+ const chain = {
987
+ sub: "agent-runner",
988
+ act: { sub: "mcp-client-42" }
989
+ };
990
+ /**
991
+ * `supportSession` and `supportSessionReadOnly` are what `supportClaims`
992
+ * mints (`act.kind: "support"`), `impersonation` what `actingAs` mints
993
+ * (`act.kind: "impersonation"`), `oauthClient` a Supabase OAuth server token
994
+ * and `agentChain` an RFC 8693 chain without `kind`.
995
+ */
996
+ const supabaseClaimFixtures = {
997
+ supportSession: {
998
+ claims: {
999
+ ...base,
1000
+ act: {
1001
+ kind: "support",
1002
+ sub: admin,
1003
+ reason: "ticket 42",
1004
+ session_id: supportSessionId,
1005
+ read_only: false
1006
+ }
1007
+ },
1008
+ expect: {
1009
+ actor: {
1010
+ kind: "support",
1011
+ id: admin,
1012
+ sessionId: supportSessionId,
1013
+ readOnly: false,
1014
+ reason: "ticket 42"
1015
+ },
1016
+ impersonator: {
1017
+ kind: "support",
1018
+ id: admin,
1019
+ sessionId: supportSessionId,
1020
+ readOnly: false,
1021
+ reason: "ticket 42"
1022
+ }
1023
+ }
1024
+ },
1025
+ supportSessionReadOnly: {
1026
+ claims: {
1027
+ ...base,
1028
+ act: {
1029
+ kind: "support",
1030
+ sub: admin,
1031
+ reason: "ticket 42",
1032
+ session_id: supportSessionId,
1033
+ read_only: true
1034
+ }
1035
+ },
1036
+ expect: {
1037
+ actor: {
1038
+ kind: "support",
1039
+ id: admin,
1040
+ sessionId: supportSessionId,
1041
+ readOnly: true,
1042
+ reason: "ticket 42"
1043
+ },
1044
+ impersonator: {
1045
+ kind: "support",
1046
+ id: admin,
1047
+ sessionId: supportSessionId,
1048
+ readOnly: true,
1049
+ reason: "ticket 42"
1050
+ }
1051
+ }
1052
+ },
1053
+ impersonation: {
1054
+ claims: {
1055
+ ...base,
1056
+ act: {
1057
+ kind: "impersonation",
1058
+ sub: admin,
1059
+ reason: "ticket 42"
1060
+ }
1061
+ },
1062
+ expect: {
1063
+ actor: {
1064
+ kind: "impersonation",
1065
+ id: admin,
1066
+ reason: "ticket 42"
1067
+ },
1068
+ impersonator: {
1069
+ kind: "impersonation",
1070
+ id: admin,
1071
+ reason: "ticket 42"
1072
+ }
1073
+ }
1074
+ },
1075
+ oauthClient: {
1076
+ claims: {
1077
+ ...base,
1078
+ client_id: client,
1079
+ scope: "openid email posts:read"
1080
+ },
1081
+ expect: {
1082
+ actor: {
1083
+ kind: "oauth-client",
1084
+ id: client
1085
+ },
1086
+ delegation: { scopes: [
1087
+ "openid",
1088
+ "email",
1089
+ "posts:read"
1090
+ ] }
1091
+ }
1092
+ },
1093
+ agentChain: {
1094
+ claims: {
1095
+ ...base,
1096
+ scope: "posts:read",
1097
+ act: chain
1098
+ },
1099
+ expect: {
1100
+ actor: {
1101
+ kind: "oauth-client",
1102
+ id: "agent-runner",
1103
+ chain
1104
+ },
1105
+ delegation: {
1106
+ scopes: ["posts:read"],
1107
+ chain
1108
+ }
1109
+ }
1110
+ }
1111
+ };
1112
+ //#endregion
973
1113
  //#region src/testing/seed.ts
974
1114
  const SEED = Symbol.for("better-supabase.seed");
975
1115
  function isSeed(value) {
@@ -1243,4 +1383,4 @@ function testWebhookSecretStore(store, options) {
1243
1383
  ]);
1244
1384
  }
1245
1385
  //#endregion
1246
- export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testNotificationChannel, testPlugin, testQueueBackend, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };
1386
+ export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, supabaseClaimFixtures, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testNotificationChannel, testPlugin, testQueueBackend, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };
@@ -1,4 +1,4 @@
1
1
  //#region src/core/version.ts
2
- const VERSION = "0.5.0";
2
+ const VERSION = "0.5.1";
3
3
  //#endregion
4
4
  export { VERSION as t };
@@ -1,68 +1,9 @@
1
1
  import { n as dbError } from "./errors-DtwXa6b8.js";
2
2
  import { t as consoleLogger } from "./logger-glM28NVN.js";
3
3
  import { n as decodeJwtPayload } from "./base64-D2gsioCY.js";
4
- import { n as impersonatorOf, r as userContext } from "./impersonation-VfaGc4Q_.js";
4
+ import { a as delegationOf, i as actorOf, n as impersonatorOf, r as userContext } from "./impersonation-BgSHAYLf.js";
5
5
  import { DEFAULT_COOKIE_OPTIONS, createChunks, isChunkLike, parseCookieHeader, serializeCookieHeader, stringFromBase64URL, stringToBase64URL } from "@supabase/ssr";
6
6
  import { extractCredentials, verifyCredentials } from "@supabase/server/core";
7
- //#region src/auth/actor.ts
8
- function isRecord(value) {
9
- return typeof value === "object" && value !== null && !Array.isArray(value);
10
- }
11
- function copyAct(level) {
12
- const entries = Object.entries(level).map(([key, value]) => [key, key === "act" && isRecord(value) ? copyAct(value) : value]);
13
- return Object.fromEntries(entries);
14
- }
15
- /**
16
- * Reads only `act` and `client_id`, the way PermDock's `actorOf` does. An
17
- * `act` that is not a chain of objects each with a non-empty `sub` is
18
- * `{ ok: false }`: the session must not pass as the user alone.
19
- */
20
- function actorOf(claims) {
21
- const outer = claims["act"];
22
- if (outer !== void 0) {
23
- let level = outer;
24
- while (level !== void 0) {
25
- if (!isRecord(level)) return {
26
- ok: false,
27
- reason: "invalid-chain"
28
- };
29
- const sub = level["sub"];
30
- if (typeof sub !== "string" || sub === "") return {
31
- ok: false,
32
- reason: "invalid-chain"
33
- };
34
- level = level["act"];
35
- }
36
- if (!isRecord(outer) || typeof outer["sub"] !== "string") return {
37
- ok: false,
38
- reason: "invalid-chain"
39
- };
40
- return {
41
- ok: true,
42
- actor: {
43
- id: outer["sub"],
44
- kind: "oauth-client",
45
- chain: copyAct(outer)
46
- }
47
- };
48
- }
49
- const client = claims["client_id"];
50
- if (typeof client === "string" && client !== "") return {
51
- ok: true,
52
- actor: {
53
- id: client,
54
- kind: "oauth-client"
55
- }
56
- };
57
- return { ok: true };
58
- }
59
- /** The OAuth `scope` claim (a space-separated string or a list); `undefined` when empty. */
60
- function delegationOf(claims) {
61
- const scope = claims["scope"];
62
- const scopes = typeof scope === "string" ? scope.split(/\s+/u).filter(Boolean) : Array.isArray(scope) ? scope.filter((item) => typeof item === "string") : [];
63
- return scopes.length > 0 ? { scopes } : void 0;
64
- }
65
- //#endregion
66
7
  //#region src/auth/refresh.ts
67
8
  /**
68
9
  * How long a finished refresh is reused for requests still carrying the old
@@ -402,7 +343,7 @@ async function checkClaims(state, options) {
402
343
  if (!actorOf(state.claims).ok) return {
403
344
  kind: "invalid",
404
345
  reason: "actor",
405
- error: dbError("unauthorized", "The token's act claim is not a chain of actors with a sub", { code: "ACTOR_INVALID" })
346
+ error: dbError("unauthorized", "The token's act claim is not a chain of actors with a sub, or has a kind other than support or impersonation", { code: "ACTOR_INVALID" })
406
347
  };
407
348
  if (!options.claims) return state;
408
349
  let outcome = options.claims["~standard"].validate(state.claims);
@@ -703,10 +644,11 @@ function toSession(auth) {
703
644
  const impersonator = impersonatorOf(auth.claims);
704
645
  const outcome = actorOf(auth.claims);
705
646
  const actor = outcome.ok ? outcome.actor : void 0;
706
- const scopes = actor ? delegationOf(auth.claims)?.scopes : void 0;
707
- const delegation = actor && (scopes || actor.chain) ? {
647
+ const client = actor?.kind === "oauth-client" ? actor : void 0;
648
+ const scopes = client ? delegationOf(auth.claims)?.scopes : void 0;
649
+ const delegation = client && (scopes || client.chain) ? {
708
650
  scopes: scopes ?? [],
709
- ...actor.chain ? { chain: actor.chain } : {}
651
+ ...client.chain ? { chain: client.chain } : {}
710
652
  } : void 0;
711
653
  return {
712
654
  kind: "user",
@@ -739,4 +681,4 @@ function toSession(auth) {
739
681
  }
740
682
  }
741
683
  //#endregion
742
- export { writeSession as _, checkAal as a, delegationOf as b, prefetchJwks as c, AUTH_CACHE_HEADERS as d, applyCookieWrites as f, sessionCookieName as g, serializeCookie as h, amrOf as i, rememberVerified as l, readSession as m, toSession as n, authContext as o, parseCookies as p, aalOf as r, clientIp as s, isAnonymousUser as t, resolveAuth as u, refreshSession as v, actorOf as y };
684
+ export { writeSession as _, checkAal as a, prefetchJwks as c, AUTH_CACHE_HEADERS as d, applyCookieWrites as f, sessionCookieName as g, serializeCookie as h, amrOf as i, rememberVerified as l, readSession as m, toSession as n, authContext as o, parseCookies as p, aalOf as r, clientIp as s, isAnonymousUser as t, resolveAuth as u, refreshSession as v };
@@ -1,30 +1,8 @@
1
1
  import { _ as DbError } from "./executor-BunPOO-k.js";
2
- import { i as InvalidReason, r as AuthState } from "./resolve-C15TJq77.js";
3
- import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-CusDgPaD.js";
2
+ import { n as Impersonator, o as AuthState, s as InvalidReason } from "./impersonation-BmePsuxz.js";
3
+ import { n as AmrEntry, t as Aal } from "./mfa-Cu96oJdw.js";
4
+ import { n as SessionActor, r as SessionDelegation } from "./actor-DaJjqD5R.js";
4
5
  import { JWTClaims, UserClaims } from "@supabase/server";
5
- //#region src/auth/actor.d.ts
6
- /** One level of an RFC 8693 `act` chain: who acts, and who acted before it. */
7
- interface ActClaim {
8
- readonly sub: string;
9
- readonly act?: ActClaim;
10
- readonly [claim: string]: unknown;
11
- }
12
- /**
13
- * The app acting for the user: the outermost `sub` of an RFC 8693 `act`
14
- * chain (prior actors stay on `chain`), else the OAuth `client_id` of a
15
- * Supabase OAuth server token.
16
- */
17
- interface SessionActor {
18
- readonly id: string;
19
- readonly kind: "oauth-client";
20
- readonly chain?: ActClaim;
21
- }
22
- /** What the user delegated to the actor: the token's OAuth `scope`. */
23
- interface SessionDelegation {
24
- readonly scopes: readonly string[];
25
- readonly chain?: ActClaim;
26
- }
27
- //#endregion
28
6
  //#region src/auth/view.d.ts
29
7
  /**
30
8
  * The verified caller as plain data: no token, no clients. Safe to return
@@ -50,19 +28,27 @@ type AuthSession<C = unknown, P = unknown> = {
50
28
  readonly aal: Aal;
51
29
  /**
52
30
  * Signed in with `signInAnonymously()` (the `is_anonymous` claim). Guards
53
- * refuse these users unless `allow` lists `'anonymous'`.
31
+ * refuse these users unless `allow` lists `'anonymous'`; `'anon'` means
32
+ * no session and never admits them.
54
33
  */
55
34
  readonly anonymous: boolean;
56
35
  /** How the user signed in (`password`, `totp`, `sso/saml`, ...). */
57
36
  readonly amr: readonly AmrEntry[];
58
- /** Set when an admin acts as this user (the `act` claim), for a banner. */
37
+ /**
38
+ * Set when an admin acts as this user in a support session or an
39
+ * impersonated session (`act.kind`), for a banner. Matches `actor`.
40
+ */
59
41
  readonly impersonator?: Impersonator;
60
42
  /**
61
- * The OAuth client or agent acting for the user (`client_id`, or the
62
- * outermost `sub` of the `act` chain), as PermDock's `actorOf` reads it.
43
+ * Who acts for the user: an OAuth client or agent (`client_id`, or an
44
+ * `act` chain without `kind`), a support session or an impersonated
45
+ * session, as PermDock's `actorOf` reads it.
63
46
  */
64
47
  readonly actor?: SessionActor;
65
- /** The scopes the user granted `actor`, plus its `act` chain. Set only with `actor`. */
48
+ /**
49
+ * The scopes the user granted an `oauth-client` actor, plus its `act`
50
+ * chain. Never set for a support or impersonated session.
51
+ */
66
52
  readonly delegation?: SessionDelegation;
67
53
  } | {
68
54
  readonly kind: "service";
@@ -81,4 +67,4 @@ type AnonReason = Extract<AuthState, {
81
67
  /** Drops the token from an `AuthState`, leaving only serializable fields. */
82
68
  declare function toSession<C, P>(auth: AuthState<C, P>): AuthSession<C, P>;
83
69
  //#endregion
84
- export { SessionDelegation as a, SessionActor as i, toSession as n, ActClaim as r, AuthSession as t };
70
+ export { toSession as n, AuthSession as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-supabase",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue, framework integrations and the better-supabase CLI.",
5
5
  "keywords": [
6
6
  "cli",
@@ -190,12 +190,12 @@
190
190
  "provenance": true
191
191
  },
192
192
  "dependencies": {
193
- "@standard-schema/spec": "1.1.0",
194
- "@supabase/middleware": "1.0.0",
195
- "@supabase/postgrest-js": "2.117.2",
196
- "@supabase/postgrest-typegen": "0.3.1",
197
- "@supabase/server": "1.9.0",
198
- "@supabase/ssr": "0.12.7"
193
+ "@standard-schema/spec": "^1.1.0",
194
+ "@supabase/middleware": "^1.0.0",
195
+ "@supabase/postgrest-js": "^2.116.0",
196
+ "@supabase/postgrest-typegen": "0.4.0",
197
+ "@supabase/server": "^1.9.0",
198
+ "@supabase/ssr": "^0.12.7"
199
199
  },
200
200
  "devDependencies": {
201
201
  "@arethetypeswrong/cli": "0.18.5",
@@ -204,13 +204,13 @@
204
204
  "@clack/prompts": "1.8.1",
205
205
  "@effect/platform-node": "4.0.0",
206
206
  "@opentelemetry/api": "1.9.1",
207
- "@orpc/server": "2.0.0-beta.40",
207
+ "@orpc/server": "2.0.0-beta.41",
208
208
  "@supabase/config": "0.11.1",
209
209
  "@supabase/supabase-js": "2.117.2",
210
210
  "@t3-oss/env-core": "0.13.11",
211
- "@tanstack/query-core": "5.104.0",
212
- "@tanstack/react-query": "5.104.0",
213
- "@types/node": "26.6.3",
211
+ "@tanstack/query-core": "5.104.1",
212
+ "@tanstack/react-query": "5.104.1",
213
+ "@types/node": "26.6.4",
214
214
  "@types/pg": "8.23.1",
215
215
  "@types/react": "19.3.0",
216
216
  "@types/react-dom": "19.3.0",
@@ -228,7 +228,7 @@
228
228
  "oxfmt": "0.71.0",
229
229
  "oxlint": "1.86.0",
230
230
  "pg": "8.23.1",
231
- "publint": "0.3.24",
231
+ "publint": "0.3.25",
232
232
  "react": "19.3.0",
233
233
  "react-dom": "19.3.0",
234
234
  "smol-toml": "1.9.0",
@@ -248,7 +248,7 @@
248
248
  "@tanstack/query-core": "^5.62.0",
249
249
  "hono": ">=4 <5",
250
250
  "next": ">=16 <17",
251
- "oxfmt": "0.66.0",
251
+ "oxfmt": ">=0.66.0 <1",
252
252
  "pg": ">=8.15 <9",
253
253
  "react": "^19",
254
254
  "temporal-polyfill": ">=1"
@@ -91,6 +91,7 @@
91
91
  "BS408",
92
92
  "BS409",
93
93
  "BS410",
94
+ "BS411",
94
95
  "BS501",
95
96
  "BS502"
96
97
  ]
@@ -61,16 +61,22 @@ A token from the Supabase OAuth server carries `client_id` and `scope`; a
61
61
  token exchanged for an agent carries an RFC 8693 `act` chain. The session
62
62
  names who acts for the user:
63
63
 
64
- | Token | `session.actor` | `session.delegation` |
65
- | ----------------------------------------------- | ------------------------------------ | -------------------- |
66
- | The user's own | not set | not set |
67
- | `client_id: "c1"`, `scope: "openid crm.read"` | `{ id: "c1", kind: "oauth-client" }` | `{ scopes: [...] }` |
68
- | `act: { sub: "agent", act: { sub: "mcp-42" } }` | `{ id: "agent", chain, ... }` | `{ scopes, chain }` |
64
+ | Token | `session.actor` | `session.delegation` |
65
+ | ------------------------------------------------ | ---------------------------------------------- | -------------------- |
66
+ | The user's own | not set | not set |
67
+ | `client_id: "c1"`, `scope: "openid crm.read"` | `{ id: "c1", kind: "oauth-client" }` | `{ scopes: [...] }` |
68
+ | `act: { sub: "agent", act: { sub: "mcp-42" } }` | `{ id: "agent", chain, ... }` | `{ scopes, chain }` |
69
+ | `act: { kind: "support", sub, session_id, ... }` | `{ kind: "support", id, sessionId, readOnly }` | not set |
70
+ | `act: { kind: "impersonation", sub, reason }` | `{ kind: "impersonation", id, reason }` | not set |
69
71
 
70
72
  - Limit what delegated tokens may do with the `scopes` guard option on
71
73
  `bs.route`, `bs.action`, `bs.handler` and `bs.middleware`. A missing
72
- scope answers 403 with an `insufficient_scope` challenge. The user's own
73
- token is never limited by scopes.
74
+ scope answers 403 with an `insufficient_scope` challenge. Only an
75
+ `oauth-client` actor is limited: the user's own token, a support session
76
+ and an impersonated session are not.
77
+ - Switch on `session.actor.kind` with a `never` default: it is
78
+ `oauth-client`, `support` or `impersonation`. An `act` with another `kind`
79
+ resolves to `invalid` with reason `actor`.
74
80
  - In MCP servers, refuse calls in `authorize` by reading
75
81
  `toSession(ctx.auth).delegation?.scopes` (the `better-supabase-api` skill).
76
82
  - RLS still decides the rows: `sub` is the user, so a client sees at most
@@ -139,6 +145,8 @@ claims, tenant or entitlements.
139
145
  - Don't downgrade an `invalid` caller to `anon`.
140
146
  - Don't build 401 or 403 JSON by hand; return the `DbError` or use `allow`
141
147
  and `scopes`.
148
+ - Don't use `allow: ['anon']` for users from `signInAnonymously()`: `'anon'`
149
+ means no session, and only `'anonymous'` admits anonymous sign-ins.
142
150
 
143
151
  ## Docs
144
152
 
@@ -96,5 +96,27 @@ the other.
96
96
 
97
97
  - Doctor BS405 measures `memberships` plus `attrs` against PermDock's 1 KB
98
98
  budget and the whole token against 2 KB; run `doctor --as <user id>`.
99
+ - For the SQL kit, set `kits.access.model: 'permdock'` and
100
+ `kits.access.functions.canAssign: 'permdock.permdock_can_assign({role}, {tenant}::text)'`
101
+ (use the manifest's `rls.schema`), then run `permdock supabase inspect --out`
102
+ before `sql add`. The kit reads the schema, the root scope and its id type
103
+ from `permdock.manifest.json`, and `sql add` stops instead of guessing when
104
+ the manifest can't give them. Every permission key the kit modules check
105
+ (`kitPermissionKeys` from `better-supabase/sql`) must be
106
+ `rowConditions: false` in `permissions.catalog.json`; map others with
107
+ `kits.<module>.permissions`. Doctor reports BS411.
108
+ - Under that model `can_user()` and `member_can()` answer for the caller only
109
+ and raise SQLSTATE `0A000` for another user. Invitations skip the inviter
110
+ re-check at accept time and notifications don't filter recipients by read
111
+ permission; check both in the app when they matter.
112
+ - The `act` claim says who acts: `kind: "support"` (support sessions,
113
+ `session.actor.kind === 'support'` with `sessionId` and `readOnly`),
114
+ `kind: "impersonation"` (`actingAs`), or no `kind` for an OAuth client or
115
+ agent chain (`oauth-client`, limited by `scopes`). Another `kind` makes the
116
+ session invalid. `session.impersonator` is set for the first two only.
117
+ `supabaseClaimFixtures` in `better-supabase/testing` has one token of each.
118
+ - Only `allow: ['anonymous']` admits `signInAnonymously()` users; `'anon'`
119
+ means no session. Pair the default `allow` with PermDock's
120
+ `rls: { anonymousSignIns: 'deny' }` in `permdock.config.ts`.
99
121
 
100
122
  Docs: https://bettersupabase.com/docs/auth/permdock.md
@@ -1,2 +0,0 @@
1
- import { n as genCommand } from "./gen-C2osRcr5.js";
2
- export { genCommand };
@@ -1,41 +0,0 @@
1
- //#region src/auth/impersonation.ts
2
- /** The `act` claim for an impersonated session. */
3
- function actClaim(options) {
4
- return {
5
- sub: options.actor,
6
- reason: options.reason
7
- };
8
- }
9
- /** The impersonator in `claims.act`, or `undefined` for a normal session. */
10
- function impersonatorOf(claims) {
11
- const act = claims["act"];
12
- if (typeof act !== "object" || act === null) return void 0;
13
- const { sub, reason, session_id, read_only } = act;
14
- if (typeof sub !== "string" || sub === "") return void 0;
15
- return {
16
- id: sub,
17
- ...typeof reason === "string" ? { reason } : {},
18
- ...typeof session_id === "string" ? { sessionId: session_id } : {},
19
- ...typeof read_only === "boolean" ? { readOnly: read_only } : {}
20
- };
21
- }
22
- /**
23
- * The repository context of a signed-in user: the actor, with the
24
- * impersonator from `act`, and the claims. Every adapter builds it here, so
25
- * plugins see the same actor on the server, in middleware and in the browser.
26
- */
27
- function userContext(user, claims) {
28
- const impersonator = impersonatorOf(claims);
29
- return {
30
- actor: {
31
- id: user.id,
32
- kind: "user",
33
- ...user.role ? { role: user.role } : {},
34
- ...user.email ? { email: user.email } : {},
35
- ...impersonator ? { impersonator: impersonator.id } : {}
36
- },
37
- claims
38
- };
39
- }
40
- //#endregion
41
- export { impersonatorOf as n, userContext as r, actClaim as t };