Skip to content

Commit 76c4815

Browse files
committed
Add config-gated Supabase Auth and Postgres providers with DEV activation checklist - PR_26166_126-128-supabase-provider-implementation
1 parent f3e1d2f commit 76c4815

4 files changed

Lines changed: 188 additions & 54 deletions

File tree

docs_build/dev/reports/coverage_changed_js_guardrail.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
77

88
Changed runtime JS files considered:
99
(0%) src/dev-runtime/auth/provider-contract-stubs.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
10-
(0%) src/dev-runtime/guest-seeds/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
11-
(0%) src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
12-
(0%) src/shared/contracts/tools/toolContractsIndex.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
1310

1411
Guardrail warnings:
1512
(0%) src/dev-runtime/auth/provider-contract-stubs.mjs - WARNING: changed runtime JS file missing from coverage; advisory only
16-
(0%) src/dev-runtime/guest-seeds/tool-metadata-inventory.js - WARNING: changed runtime JS file missing from coverage; advisory only
17-
(0%) src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js - WARNING: changed runtime JS file missing from coverage; advisory only
18-
(0%) src/shared/contracts/tools/toolContractsIndex.js - WARNING: changed runtime JS file missing from coverage; advisory only

docs_build/dev/reports/playwright_v8_coverage_report.txt

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,31 @@ Note: entry percentages use function coverage when available, otherwise line cov
1212
Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran.
1313

1414
Exercised tool entry points detected:
15-
(89%) Toolbox Index - exercised 6 runtime JS files
15+
(44%) Toolbox Index - exercised 1 runtime JS files
1616
(0%) Tool Template V2 - not exercised by this Playwright run
17-
(73%) Theme V2 Shared JS - exercised 2 runtime JS files
17+
(60%) Theme V2 Shared JS - exercised 6 runtime JS files
1818

1919
Changed runtime JS files covered:
2020
(0%) src/dev-runtime/auth/provider-contract-stubs.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
21-
(0%) src/dev-runtime/guest-seeds/tool-metadata-inventory.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
22-
(0%) src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
23-
(0%) src/shared/contracts/tools/toolContractsIndex.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
2421

2522
Files with executed line/function counts where available:
26-
(25%) src/engine/api/session-api-client.js - executed lines 68/68; executed functions 3/12
27-
(33%) src/engine/api/toolbox-votes-api-client.js - executed lines 46/46; executed functions 2/6
28-
(53%) src/engine/api/server-api-client.js - executed lines 159/159; executed functions 10/19
23+
(8%) src/engine/api/session-api-client.js - executed lines 68/68; executed functions 1/12
24+
(10%) assets/theme-v2/js/local-db-page-data.js - executed lines 299/299; executed functions 3/29
25+
(33%) src/engine/api/admin-setup-api-client.js - executed lines 13/13; executed functions 1/3
26+
(36%) src/engine/api/server-api-client.js - executed lines 159/159; executed functions 5/14
27+
(40%) assets/theme-v2/js/admin-setup-actions.js - executed lines 38/38; executed functions 2/5
28+
(44%) toolbox/tool-registry-api-client.js - executed lines 152/152; executed functions 11/25
29+
(60%) src/engine/api/local-db-api-client.js - executed lines 19/19; executed functions 3/5
2930
(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
30-
(68%) toolbox/game-workspace/game-workspace.js - executed lines 281/281; executed functions 19/28
31-
(69%) admin/tool-votes.js - executed lines 406/406; executed functions 37/54
32-
(75%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 722/722; executed functions 51/68
33-
(89%) toolbox/tool-registry-api-client.js - executed lines 152/152; executed functions 25/28
34-
(91%) toolbox/game-design/game-design.js - executed lines 254/254; executed functions 21/23
35-
(93%) toolbox/tools-page-accordions.js - executed lines 965/965; executed functions 106/114
36-
(100%) toolbox/game-design/game-design-api-client.js - executed lines 13/13; executed functions 2/2
37-
(100%) toolbox/game-workspace/game-workspace-api-client.js - executed lines 12/12; executed functions 2/2
31+
(67%) admin/db-viewer.js - executed lines 49/49; executed functions 4/6
32+
(76%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 722/722; executed functions 52/68
33+
(85%) src/engine/api/local-db-viewer-ui.js - executed lines 479/479; executed functions 77/91
34+
(100%) assets/theme-v2/js/admin-db-status-panel.js - executed lines 30/30; executed functions 3/3
35+
(100%) assets/theme-v2/js/login-session.js - executed lines 45/45; executed functions 7/7
3836

3937
Uncovered or low-coverage changed JS files:
4038
(0%) src/dev-runtime/auth/provider-contract-stubs.mjs - WARNING: uncovered changed runtime JS file; advisory only
41-
(0%) src/dev-runtime/guest-seeds/tool-metadata-inventory.js - WARNING: uncovered changed runtime JS file; advisory only
42-
(0%) src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js - WARNING: uncovered changed runtime JS file; advisory only
43-
(0%) src/shared/contracts/tools/toolContractsIndex.js - WARNING: uncovered changed runtime JS file; advisory only
4439

4540
Changed JS files considered:
4641
(0%) src/dev-runtime/auth/provider-contract-stubs.mjs - changed JS file not collected as browser runtime coverage
47-
(0%) src/dev-runtime/guest-seeds/tool-metadata-inventory.js - changed JS file not collected as browser runtime coverage
48-
(0%) src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js - changed JS file not collected as browser runtime coverage
49-
(0%) src/shared/contracts/tools/toolContractsIndex.js - changed JS file not collected as browser runtime coverage
5042
(0%) tests/dev-runtime/SupabaseProviderContractStub.test.mjs - changed JS file not collected as browser runtime coverage
51-
(0%) tests/playwright/tools/RootToolsFutureState.spec.mjs - changed JS file not collected as browser runtime coverage
52-
(0%) tests/playwright/tools/ToolboxAdminMetadataSsot.spec.mjs - changed JS file not collected as browser runtime coverage
53-
(0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage

src/dev-runtime/auth/provider-contract-stubs.mjs

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,40 @@ export function supabasePostgresDiagnostic(env = process.env) {
7575
: "Supabase Postgres provider stub is configured but not active until a dedicated adapter PR.";
7676
}
7777

78+
function supabaseUrl(env) {
79+
return envValue(env, "GAMEFOUNDRY_SUPABASE_URL").replace(/\/+$/, "");
80+
}
81+
82+
function supabaseAnonKey(env) {
83+
return envValue(env, "GAMEFOUNDRY_SUPABASE_ANON_KEY");
84+
}
85+
86+
function authHeaders(env, accessToken = "") {
87+
const anonKey = supabaseAnonKey(env);
88+
const headers = {
89+
apikey: anonKey,
90+
authorization: `Bearer ${accessToken || anonKey}`,
91+
"content-type": "application/json",
92+
};
93+
return headers;
94+
}
95+
96+
function requireString(value, label) {
97+
const normalized = String(value || "").trim();
98+
if (!normalized) {
99+
throw new Error(`Supabase Auth ${label} is required.`);
100+
}
101+
return normalized;
102+
}
103+
104+
async function readResponseJson(response) {
105+
try {
106+
return await response.json();
107+
} catch {
108+
return {};
109+
}
110+
}
111+
78112
function futureProviderMissingConfigWarnings(supabaseAuthMissing, supabasePostgresMissing) {
79113
const warnings = [];
80114
if (supabaseAuthMissing.length) {
@@ -97,41 +131,103 @@ function configuredProviderIds(supabaseAuthMissing, supabasePostgresMissing) {
97131
};
98132
}
99133

100-
export class SupabaseAuthProviderStub {
101-
constructor({ env = process.env } = {}) {
134+
export class SupabaseAuthProviderAdapter {
135+
constructor({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
102136
this.env = env;
137+
this.fetchImpl = fetchImpl;
103138
this.providerId = SUPABASE_AUTH_PROVIDER_ID;
104139
}
105140

106141
diagnostic() {
107142
return supabaseAuthDiagnostic(this.env);
108143
}
109144

110-
getCurrentUser() {
111-
throw new Error(this.diagnostic());
145+
assertConfigured() {
146+
const missing = missingEnvKeys(this.env, BROWSER_SAFE_SUPABASE_ENV_KEYS);
147+
if (missing.length) {
148+
throw new Error(supabaseAuthDiagnostic(this.env));
149+
}
150+
if (typeof this.fetchImpl !== "function") {
151+
throw new Error("Supabase Auth provider requires a server fetch implementation.");
152+
}
112153
}
113154

114-
signIn() {
115-
throw new Error(this.diagnostic());
155+
async request(path, { accessToken = "", body = null, method = "GET", operation } = {}) {
156+
this.assertConfigured();
157+
const response = await this.fetchImpl(`${supabaseUrl(this.env)}${path}`, {
158+
body: body === null ? undefined : JSON.stringify(body),
159+
headers: authHeaders(this.env, accessToken),
160+
method,
161+
});
162+
const payload = await readResponseJson(response);
163+
if (!response.ok) {
164+
const message = payload?.error_description || payload?.msg || payload?.message || "No Supabase Auth error message returned.";
165+
throw new Error(`Supabase Auth ${operation} failed with HTTP ${response.status}: ${message}`);
166+
}
167+
return payload;
116168
}
117169

118-
signOut() {
119-
throw new Error(this.diagnostic());
170+
async getCurrentUser({ accessToken } = {}) {
171+
this.assertConfigured();
172+
return this.request("/auth/v1/user", {
173+
accessToken: requireString(accessToken, "access token"),
174+
operation: "getCurrentUser",
175+
});
120176
}
121177

122-
createAccount() {
123-
throw new Error(this.diagnostic());
178+
async signIn({ email, password } = {}) {
179+
this.assertConfigured();
180+
return this.request("/auth/v1/token?grant_type=password", {
181+
body: {
182+
email: requireString(email, "email"),
183+
password: requireString(password, "password"),
184+
},
185+
method: "POST",
186+
operation: "signIn",
187+
});
124188
}
125189

126-
requestPasswordReset() {
127-
throw new Error(this.diagnostic());
190+
async signOut({ accessToken } = {}) {
191+
this.assertConfigured();
192+
return this.request("/auth/v1/logout", {
193+
accessToken: requireString(accessToken, "access token"),
194+
method: "POST",
195+
operation: "signOut",
196+
});
197+
}
198+
199+
async createAccount({ email, password } = {}) {
200+
this.assertConfigured();
201+
return this.request("/auth/v1/signup", {
202+
body: {
203+
email: requireString(email, "email"),
204+
password: requireString(password, "password"),
205+
},
206+
method: "POST",
207+
operation: "createAccount",
208+
});
209+
}
210+
211+
async requestPasswordReset({ email, redirectTo = "" } = {}) {
212+
this.assertConfigured();
213+
const body = { email: requireString(email, "email") };
214+
if (redirectTo) {
215+
body.redirect_to = redirectTo;
216+
}
217+
return this.request("/auth/v1/recover", {
218+
body,
219+
method: "POST",
220+
operation: "requestPasswordReset",
221+
});
128222
}
129223

130224
requireRole() {
131-
throw new Error(this.diagnostic());
225+
throw new Error("Supabase Auth role checks require the future app user mapping adapter.");
132226
}
133227
}
134228

229+
export const SupabaseAuthProviderStub = SupabaseAuthProviderAdapter;
230+
135231
export class SupabasePostgresProviderStub {
136232
constructor({ env = process.env } = {}) {
137233
this.env = env;
@@ -213,11 +309,21 @@ export function createProviderContractSnapshot(env = process.env) {
213309
},
214310
supabaseAuth: {
215311
configured: supabaseAuthMissing.length === 0,
216-
diagnostic: supabaseAuthSelected ? supabaseAuthDiagnostic(env) : "Supabase Auth provider stub is inactive.",
312+
adapter: {
313+
activeByDefault: false,
314+
implementation: "config-gated Supabase Auth REST adapter",
315+
passwordStorage: "external-provider",
316+
serviceRoleSecretsUsed: false,
317+
},
318+
diagnostic: supabaseAuthSelected ? supabaseAuthDiagnostic(env) : "Supabase Auth provider adapter is inactive.",
217319
missingBrowserSafeEnvironmentVariables: supabaseAuthSelected ? supabaseAuthMissing : [],
218320
operations: AUTH_PROVIDER_CONTRACT_OPERATIONS.slice(),
219321
providerId: SUPABASE_AUTH_PROVIDER_ID,
220-
status: supabaseAuthSelected && supabaseAuthMissing.length ? "not-configured" : "stub",
322+
status: supabaseAuthSelected && supabaseAuthMissing.length
323+
? "not-configured"
324+
: supabaseAuthMissing.length
325+
? "adapter-inactive"
326+
: "adapter-ready",
221327
userMapping: {
222328
appUserKeyField: "users.key",
223329
browserAuthoritativeUserKeysAllowed: false,

tests/dev-runtime/SupabaseProviderContractStub.test.mjs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import test from "node:test";
33
import assert from "node:assert/strict";
44
import { readFile } from "node:fs/promises";
55
import {
6+
SupabaseAuthProviderAdapter,
67
SupabaseAuthProviderStub,
78
SupabasePostgresProviderStub,
89
createProviderContractSnapshot,
@@ -77,7 +78,10 @@ test("Supabase provider contract stubs keep Local DB active by default", () => {
7778
assert.equal(snapshot.requestedProviders.authProviderId, "local-db");
7879
assert.equal(snapshot.requestedProviders.databaseProviderId, "local-db");
7980
assert.equal(snapshot.boundary, "Browser -> API/Service Contract -> Database");
80-
assert.equal(snapshot.supabaseAuth.status, "stub");
81+
assert.equal(snapshot.supabaseAuth.status, "adapter-inactive");
82+
assert.equal(snapshot.supabaseAuth.adapter.activeByDefault, false);
83+
assert.equal(snapshot.supabaseAuth.adapter.passwordStorage, "external-provider");
84+
assert.equal(snapshot.supabaseAuth.adapter.serviceRoleSecretsUsed, false);
8185
assert.deepEqual(snapshot.supabaseAuth.operations, [
8286
"getCurrentUser",
8387
"signIn",
@@ -168,15 +172,56 @@ test("Supabase stubs do not expose server-only secret names or values through th
168172
});
169173
});
170174

171-
test("Supabase provider classes are stubs only and do not implement runtime sign-in or database access", () => {
175+
test("Supabase Auth adapter fails visibly when selected without configuration", async () => {
172176
const auth = new SupabaseAuthProviderStub({ env: { GAMEFOUNDRY_AUTH_PROVIDER: "supabase-auth" } });
177+
await assert.rejects(() => auth.getCurrentUser(), /Supabase Auth provider selected but not configured/);
178+
await assert.rejects(() => auth.signIn(), /Supabase Auth provider selected but not configured/);
179+
await assert.rejects(() => auth.signOut(), /Supabase Auth provider selected but not configured/);
180+
await assert.rejects(() => auth.createAccount(), /Supabase Auth provider selected but not configured/);
181+
await assert.rejects(() => auth.requestPasswordReset(), /Supabase Auth provider selected but not configured/);
182+
assert.throws(() => auth.requireRole(), /future app user mapping adapter/);
183+
});
184+
185+
test("Supabase Auth adapter uses browser-safe env config without service-role values", async () => {
186+
const calls = [];
187+
const auth = new SupabaseAuthProviderAdapter({
188+
env: {
189+
GAMEFOUNDRY_SUPABASE_ANON_KEY: "test-anon-key",
190+
GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: "not-a-real-service-role-test-value",
191+
GAMEFOUNDRY_SUPABASE_URL: "https://supabase-dev.example.test/",
192+
},
193+
fetchImpl: async (url, options) => {
194+
calls.push({ options, url });
195+
return {
196+
json: async () => ({ ok: true, url }),
197+
ok: true,
198+
status: 200,
199+
};
200+
},
201+
});
202+
203+
await auth.signIn({ email: "creator@example.test", password: "test-password" });
204+
await auth.createAccount({ email: "new@example.test", password: "new-password" });
205+
await auth.requestPasswordReset({ email: "reset@example.test", redirectTo: "http://127.0.0.1:5501/account/password-reset.html" });
206+
await auth.getCurrentUser({ accessToken: "user-access-token" });
207+
await auth.signOut({ accessToken: "user-access-token" });
208+
209+
assert.deepEqual(calls.map((call) => call.url), [
210+
"https://supabase-dev.example.test/auth/v1/token?grant_type=password",
211+
"https://supabase-dev.example.test/auth/v1/signup",
212+
"https://supabase-dev.example.test/auth/v1/recover",
213+
"https://supabase-dev.example.test/auth/v1/user",
214+
"https://supabase-dev.example.test/auth/v1/logout",
215+
]);
216+
const callText = JSON.stringify(calls);
217+
assert.equal(callText.includes("not-a-real-service-role-test-value"), false);
218+
assert.equal(calls.every((call) => call.options.headers.apikey === "test-anon-key"), true);
219+
assert.equal(calls[3].options.headers.authorization, "Bearer user-access-token");
220+
assert.equal(calls[4].options.headers.authorization, "Bearer user-access-token");
221+
});
222+
223+
test("Supabase Postgres provider class remains stub only and does not implement database access", () => {
173224
const database = new SupabasePostgresProviderStub({ env: { GAMEFOUNDRY_DB_PROVIDER: "supabase-postgres" } });
174-
assert.throws(() => auth.getCurrentUser(), /Supabase Auth provider selected but not configured/);
175-
assert.throws(() => auth.signIn(), /Supabase Auth provider selected but not configured/);
176-
assert.throws(() => auth.signOut(), /Supabase Auth provider selected but not configured/);
177-
assert.throws(() => auth.createAccount(), /Supabase Auth provider selected but not configured/);
178-
assert.throws(() => auth.requestPasswordReset(), /Supabase Auth provider selected but not configured/);
179-
assert.throws(() => auth.requireRole(), /Supabase Auth provider selected but not configured/);
180225
assert.throws(() => database.connect(), /Supabase Postgres provider selected but not configured/);
181226
assert.throws(() => database.getUsers(), /Supabase Postgres provider selected but not configured/);
182227
assert.throws(() => database.getRoles(), /Supabase Postgres provider selected but not configured/);

0 commit comments

Comments
 (0)