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.
- package/README.md +133 -16
- package/dist/actor-DaJjqD5R.d.ts +43 -0
- package/dist/cli/bin.js +1 -1
- package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
- package/dist/cli/gen-CIdSQZad.js +2 -0
- package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
- package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
- package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
- package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
- package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
- package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
- package/dist/edge/index.d.ts +2 -2
- package/dist/edge/index.js +2 -2
- package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
- package/dist/hono/index.d.ts +3 -3
- package/dist/hono/index.js +2 -2
- package/dist/impersonation-BgSHAYLf.js +133 -0
- package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
- package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
- package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
- package/dist/next/index.d.ts +6 -6
- package/dist/next/index.js +3 -3
- package/dist/orpc/index.d.ts +2 -2
- package/dist/orpc/index.js +1 -1
- package/dist/otel/index.js +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/server.d.ts +5 -5
- package/dist/react/session.d.ts +2 -2
- package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
- package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
- package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
- package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
- package/dist/server/index.d.ts +9 -8
- package/dist/server/index.js +5 -5
- package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
- package/dist/sql/index.d.ts +38 -3
- package/dist/sql/index.js +237 -118
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +1 -1
- package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
- package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
- package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
- package/dist/testing/index.d.ts +36 -4
- package/dist/testing/index.js +141 -1
- package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
- package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
- package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
- package/package.json +13 -13
- package/schemas/doctor-report-v1.json +1 -0
- package/skills/better-supabase-auth/SKILL.md +15 -7
- package/skills/better-supabase-auth/references/permdock.md +22 -0
- package/dist/cli/gen-BJc9vdyu.js +0 -2
- package/dist/impersonation-VfaGc4Q_.js +0 -41
package/dist/testing/index.js
CHANGED
|
@@ -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,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-
|
|
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
|
|
707
|
-
const
|
|
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
|
-
...
|
|
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,
|
|
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 {
|
|
3
|
-
import { n as AmrEntry,
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
|
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.
|
|
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.
|
|
196
|
-
"@supabase/postgrest-typegen": "0.
|
|
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.
|
|
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.
|
|
212
|
-
"@tanstack/react-query": "5.104.
|
|
213
|
-
"@types/node": "26.6.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
65
|
-
|
|
|
66
|
-
| The user's own
|
|
67
|
-
| `client_id: "c1"`, `scope: "openid crm.read"`
|
|
68
|
-
| `act: { sub: "agent", act: { sub: "mcp-42" } }`
|
|
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.
|
|
73
|
-
|
|
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
|
package/dist/cli/gen-BJc9vdyu.js
DELETED
|
@@ -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 };
|