@specific.dev/spectest 0.45.0 → 0.46.0
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/dist/components/supabase-config.d.ts +89 -0
- package/dist/components/supabase-config.js +447 -0
- package/dist/components/supabase.d.ts +124 -84
- package/dist/components/supabase.js +395 -136
- package/package.json +1 -1
- package/src/components/supabase-config.test.ts +261 -0
- package/src/components/supabase-config.ts +543 -0
- package/src/components/supabase-dir.test.ts +287 -0
- package/src/components/supabase.ts +581 -230
|
@@ -49,12 +49,17 @@
|
|
|
49
49
|
// matching the `name` option (default `"supabase"`) since the derived surface
|
|
50
50
|
// (`sb.url`, `sb.appEnv`) embeds it — a mismatch errors at load.
|
|
51
51
|
import { createHmac } from "node:crypto";
|
|
52
|
-
import { existsSync } from "node:fs";
|
|
52
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
53
53
|
import { readdir, stat } from "node:fs/promises";
|
|
54
54
|
import { dirname, join } from "node:path";
|
|
55
55
|
import { serviceGroup, } from "../index.js";
|
|
56
56
|
import { SQL } from "../sql.js";
|
|
57
|
+
import { apple } from "./apple.js";
|
|
57
58
|
import { email, emailHelpers } from "./email.js";
|
|
59
|
+
import { github } from "./github.js";
|
|
60
|
+
import { google } from "./google.js";
|
|
61
|
+
import { microsoft } from "./microsoft.js";
|
|
62
|
+
import { mapSupabaseConfig, parseSupabaseConfigFile, } from "./supabase-config.js";
|
|
58
63
|
// ──────────────────────────────────────────────────────────────────────────
|
|
59
64
|
// Pinned upstream image tags (github.com/supabase/supabase docker-compose.yml,
|
|
60
65
|
// current master). Bump together; each is pulled through the host Hub mirror on
|
|
@@ -68,10 +73,89 @@ const IMAGES = {
|
|
|
68
73
|
realtime: "supabase/realtime:v2.102.3",
|
|
69
74
|
storage: "supabase/storage-api:v1.60.4",
|
|
70
75
|
imgproxy: "darthsim/imgproxy:v3.30.1",
|
|
71
|
-
meta: "supabase/postgres-meta:v0.96.6",
|
|
72
|
-
studio: "supabase/studio:2026.07.07-sha-a6a04f2",
|
|
73
76
|
functions: "supabase/edge-runtime:v1.74.0",
|
|
77
|
+
// Serves custom mail templates to GoTrue over HTTP (see the `templates`
|
|
78
|
+
// part). Tiny, and only pulled by a stack that configures a template.
|
|
79
|
+
templates: "busybox:1.37",
|
|
74
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Social sign-in providers this stack can serve, by the name GoTrue (and
|
|
83
|
+
* `[auth.external.<name>]`) uses. Each factory builds one container answering
|
|
84
|
+
* at that provider's **real** endpoints — `accounts.google.com`,
|
|
85
|
+
* `login.microsoftonline.com`, … — with DNS, a CA-trusted certificate and a
|
|
86
|
+
* proxy pointing there, so GoTrue performs the ordinary flow against the
|
|
87
|
+
* ordinary URLs and never learns it is under test.
|
|
88
|
+
*
|
|
89
|
+
* A provider that is enabled in the config and absent here is reported rather
|
|
90
|
+
* than served: there is nothing to point GoTrue at.
|
|
91
|
+
*/
|
|
92
|
+
const EXTERNAL_PROVIDERS = {
|
|
93
|
+
google: (client) => google({ client }),
|
|
94
|
+
// `azure` is what GoTrue calls Entra ID.
|
|
95
|
+
azure: (client) => microsoft({ client }),
|
|
96
|
+
github: (client) => github({ client }),
|
|
97
|
+
apple: (client) => apple({ client }),
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The client credentials GoTrue presents to a provider. Taken from the config
|
|
101
|
+
* where it holds a literal, and otherwise made up: the file's values are
|
|
102
|
+
* `env(NAME)` references to a shell this environment does not have, and no
|
|
103
|
+
* real credential is meaningful against an emulator. Only GoTrue and the
|
|
104
|
+
* provider have to agree, and both are ours.
|
|
105
|
+
*/
|
|
106
|
+
function providerCredentials(providerName, declared, env) {
|
|
107
|
+
const read = (from) => from === undefined ? undefined : env?.[from];
|
|
108
|
+
return {
|
|
109
|
+
clientId: declared.clientId ?? read(declared.clientIdFrom) ?? `spectest-${providerName}-client`,
|
|
110
|
+
clientSecret: declared.secret ?? read(declared.secretFrom) ?? `spectest-${providerName}-secret`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The edge runtime, with every remote module its functions import already in
|
|
115
|
+
* the module cache.
|
|
116
|
+
*
|
|
117
|
+
* A Supabase function may import straight from a URL
|
|
118
|
+
* (`import Fuse from "https://deno.land/x/fuse@v6.4.1/…"`), and Deno fetches
|
|
119
|
+
* that when the worker boots. Inside a hermetic VM that is a network call at
|
|
120
|
+
* test time — slow at best, and a failed boot at worst.
|
|
121
|
+
*
|
|
122
|
+
* So it is done at **build** time instead. `edge-runtime bundle` walks the
|
|
123
|
+
* module graph and, unless told otherwise, fills `DENO_DIR` as it goes; the
|
|
124
|
+
* eszip it produces is thrown away, since all we want is the cache. Docker
|
|
125
|
+
* builds run on the host builder, which has ordinary network access, and the
|
|
126
|
+
* cache rides into the image as a layer — so at run time the worker resolves
|
|
127
|
+
* those URLs locally and never reaches for the network.
|
|
128
|
+
*
|
|
129
|
+
* One synthetic entrypoint imports every function, so the whole set is one
|
|
130
|
+
* graph rather than one per function. Bundling is best-effort: a function that
|
|
131
|
+
* fails to bundle (a type error, an import that only resolves at run time)
|
|
132
|
+
* leaves the cache thinner and is never a build failure — the worker then
|
|
133
|
+
* behaves exactly as it did before.
|
|
134
|
+
*/
|
|
135
|
+
function functionsDockerfile(fnRelPath) {
|
|
136
|
+
return `FROM ${IMAGES.functions}
|
|
137
|
+
ENV DENO_DIR=${DENO_DIR}
|
|
138
|
+
COPY ${fnRelPath} /warm/functions
|
|
139
|
+
RUN set -eu; \
|
|
140
|
+
: > /warm/all.ts; \
|
|
141
|
+
for d in /warm/functions/*/; do \
|
|
142
|
+
[ -f "$d/index.ts" ] || continue; \
|
|
143
|
+
n=$(basename "$d"); \
|
|
144
|
+
printf 'import "./functions/%s/index.ts";\\n' "$n" >> /warm/all.ts; \
|
|
145
|
+
done; \
|
|
146
|
+
edge-runtime bundle --entrypoint /warm/all.ts --output /tmp/warm.eszip --quiet || true; \
|
|
147
|
+
rm -rf /tmp/warm.eszip /warm
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
/** Where the module cache lives in the edge-runtime image, written at build
|
|
151
|
+
* time and read at run time. Outside the repo mount, deliberately. */
|
|
152
|
+
const DENO_DIR = "/deno-dir";
|
|
153
|
+
/** Where the project root is mounted in the edge-runtime container. Functions
|
|
154
|
+
* keep their position relative to it, so an import that reaches outside the
|
|
155
|
+
* functions directory resolves exactly as it does in the repo. */
|
|
156
|
+
const REPO_MOUNT = "/repo";
|
|
157
|
+
/** Where the mail-template server listens; upstream's CLI uses the same port. */
|
|
158
|
+
const TEMPLATE_PORT = 8088;
|
|
75
159
|
/**
|
|
76
160
|
* The project root inside the VM. A service's `volumes[].source` is a static
|
|
77
161
|
* string in the wire config, so the edge runtime's bind mount cannot go
|
|
@@ -81,6 +165,10 @@ const IMAGES = {
|
|
|
81
165
|
function projectRootPath() {
|
|
82
166
|
return process.env.SPECTEST_WORKSPACE ?? "/workspace";
|
|
83
167
|
}
|
|
168
|
+
/** A caller-supplied path: absolute as given, otherwise project-root relative. */
|
|
169
|
+
function absProjectPath(p) {
|
|
170
|
+
return p.startsWith("/") ? p : join(projectRootPath(), p);
|
|
171
|
+
}
|
|
84
172
|
// Supabase's well-known demo secret (≥32 chars). Local-only; override via opts.
|
|
85
173
|
const DEFAULT_JWT_SECRET = "your-super-secret-jwt-token-with-at-least-32-characters-long";
|
|
86
174
|
// Fixed issued-at / expiry for the derived API keys. Constant (not `Date.now`)
|
|
@@ -495,6 +583,36 @@ services:
|
|
|
495
583
|
if auth == nil or auth == "" or auth:find("^%s*$") then
|
|
496
584
|
kong.service.request.clear_header("authorization")
|
|
497
585
|
end
|
|
586
|
+
# >>> oauthserver (added only when the config enables the OAuth server)
|
|
587
|
+
# The OAuth-server endpoints must be reachable by a client that has no
|
|
588
|
+
# Supabase API key at all — a third-party connector knows only what the
|
|
589
|
+
# authorization-server metadata advertises. So they bypass the key-auth on
|
|
590
|
+
# /auth/v1/ below, exactly as /auth/v1/verify and /auth/v1/callback do,
|
|
591
|
+
# and the client's own Authorization header (a user token at the consent
|
|
592
|
+
# step, client credentials at the token endpoint) passes through untouched.
|
|
593
|
+
# RFC 8414 discovery. The issuer carries a path (.../auth/v1), so a compliant
|
|
594
|
+
# client asks for the metadata with that path INSERTED after the well-known
|
|
595
|
+
# segment — which is where hosted Supabase publishes it, and what an MCP
|
|
596
|
+
# connector will request. GoTrue only registers the bare path, so map it.
|
|
597
|
+
- name: well-known-oauth-issuer-path
|
|
598
|
+
url: http://auth:9999/.well-known/oauth-authorization-server
|
|
599
|
+
routes:
|
|
600
|
+
- name: well-known-oauth-issuer-path
|
|
601
|
+
strip_path: true
|
|
602
|
+
paths:
|
|
603
|
+
- /.well-known/oauth-authorization-server/auth/v1
|
|
604
|
+
plugins:
|
|
605
|
+
- name: cors
|
|
606
|
+
- name: auth-v1-open-oauth
|
|
607
|
+
url: http://auth:9999/oauth/
|
|
608
|
+
routes:
|
|
609
|
+
- name: auth-v1-open-oauth
|
|
610
|
+
strip_path: true
|
|
611
|
+
paths:
|
|
612
|
+
- /auth/v1/oauth/
|
|
613
|
+
plugins:
|
|
614
|
+
- name: cors
|
|
615
|
+
# <<< oauthserver
|
|
498
616
|
# >>> functions (dropped when the edge runtime is off)
|
|
499
617
|
- name: functions-v1
|
|
500
618
|
url: http://functions:9000/
|
|
@@ -516,34 +634,6 @@ services:
|
|
|
516
634
|
- /.well-known/oauth-authorization-server
|
|
517
635
|
plugins:
|
|
518
636
|
- name: cors
|
|
519
|
-
- name: meta
|
|
520
|
-
url: http://meta:8080/
|
|
521
|
-
routes:
|
|
522
|
-
- name: meta-all
|
|
523
|
-
strip_path: true
|
|
524
|
-
paths:
|
|
525
|
-
- /pg/
|
|
526
|
-
plugins:
|
|
527
|
-
- name: key-auth
|
|
528
|
-
config:
|
|
529
|
-
hide_credentials: false
|
|
530
|
-
- name: acl
|
|
531
|
-
config:
|
|
532
|
-
hide_groups_header: true
|
|
533
|
-
allow:
|
|
534
|
-
- admin
|
|
535
|
-
- name: dashboard
|
|
536
|
-
url: http://studio:3000/
|
|
537
|
-
routes:
|
|
538
|
-
- name: dashboard-all
|
|
539
|
-
strip_path: true
|
|
540
|
-
paths:
|
|
541
|
-
- /
|
|
542
|
-
plugins:
|
|
543
|
-
- name: cors
|
|
544
|
-
- name: basic-auth
|
|
545
|
-
config:
|
|
546
|
-
hide_credentials: true
|
|
547
637
|
`;
|
|
548
638
|
// ──────────────────────────────────────────────────────────────────────────
|
|
549
639
|
// Edge functions — the runtime's "main service": the request router that
|
|
@@ -558,9 +648,15 @@ services:
|
|
|
558
648
|
// with, and Web Crypto verifies it in a few lines with no dependency.
|
|
559
649
|
// ──────────────────────────────────────────────────────────────────────────
|
|
560
650
|
const FUNCTIONS_MAIN_TS = String.raw `// spectest-generated edge-functions router. Do not edit.
|
|
561
|
-
const FUNCTIONS_DIR = "/
|
|
651
|
+
const FUNCTIONS_DIR = Deno.env.get("FUNCTIONS_DIR") ?? "/repo/supabase/functions";
|
|
562
652
|
const JWT_SECRET = Deno.env.get("JWT_SECRET") ?? "";
|
|
563
653
|
const VERIFY_JWT = Deno.env.get("VERIFY_JWT") === "true";
|
|
654
|
+
// Per-function overrides (the project's config file). Hosted Supabase decides
|
|
655
|
+
// this per function, so the global flag is only the default for a function the
|
|
656
|
+
// map says nothing about.
|
|
657
|
+
const VERIFY_JWT_MAP: Record<string, boolean> = JSON.parse(
|
|
658
|
+
Deno.env.get("VERIFY_JWT_MAP") ?? "{}",
|
|
659
|
+
);
|
|
564
660
|
|
|
565
661
|
function json(body: unknown, status: number): Response {
|
|
566
662
|
return new Response(JSON.stringify(body), {
|
|
@@ -612,7 +708,7 @@ Deno.serve(async (req: Request) => {
|
|
|
612
708
|
const name = pathname.split("/")[1] ?? "";
|
|
613
709
|
if (name === "") return json({ msg: "missing function name in request" }, 400);
|
|
614
710
|
|
|
615
|
-
if (VERIFY_JWT && req.method !== "OPTIONS") {
|
|
711
|
+
if ((VERIFY_JWT_MAP[name] ?? VERIFY_JWT) && req.method !== "OPTIONS") {
|
|
616
712
|
const header = req.headers.get("authorization") ?? "";
|
|
617
713
|
const [scheme, token] = header.split(" ");
|
|
618
714
|
if (scheme !== "Bearer" || !token || !(await validJwt(token))) {
|
|
@@ -664,6 +760,7 @@ function stripMarkedBlock(yaml, tag, keep) {
|
|
|
664
760
|
function buildKongYaml(key, vals) {
|
|
665
761
|
let y = KONG_YML_RAW;
|
|
666
762
|
y = stripMarkedBlock(y, "functions", vals.functions);
|
|
763
|
+
y = stripMarkedBlock(y, "oauthserver", vals.oauthServer);
|
|
667
764
|
// Extra API keys land where kong-entrypoint.sh's opaque keys would: as
|
|
668
765
|
// additional keyauth credentials on the matching consumer. The template's
|
|
669
766
|
// placeholder lines are either expanded here or dropped by the filter below.
|
|
@@ -678,8 +775,6 @@ function buildKongYaml(key, vals) {
|
|
|
678
775
|
.replaceAll("http://auth:9999", `http://${key("auth")}:9999`)
|
|
679
776
|
.replaceAll("http://rest:3000", `http://${key("rest")}:3000`)
|
|
680
777
|
.replaceAll("http://storage:5000", `http://${key("storage")}:5000`)
|
|
681
|
-
.replaceAll("http://meta:8080", `http://${key("meta")}:8080`)
|
|
682
|
-
.replaceAll("http://studio:3000", `http://${key("studio")}:3000`)
|
|
683
778
|
.replaceAll("http://functions:9000", `http://${key("functions")}:9000`)
|
|
684
779
|
.replaceAll("realtime-dev.supabase-realtime", `realtime-dev.${key("realtime")}`);
|
|
685
780
|
// Lua expressions first (they contain no `$SUPABASE_*` tokens).
|
|
@@ -691,9 +786,7 @@ function buildKongYaml(key, vals) {
|
|
|
691
786
|
// API keys + dashboard credentials.
|
|
692
787
|
y = y
|
|
693
788
|
.replaceAll("$SUPABASE_ANON_KEY", vals.anonKey)
|
|
694
|
-
.replaceAll("$SUPABASE_SERVICE_KEY", vals.serviceRoleKey)
|
|
695
|
-
.replaceAll("$DASHBOARD_USERNAME", vals.dashboardUsername)
|
|
696
|
-
.replaceAll("$DASHBOARD_PASSWORD", vals.dashboardPassword);
|
|
789
|
+
.replaceAll("$SUPABASE_SERVICE_KEY", vals.serviceRoleKey);
|
|
697
790
|
// Drop the unconfigured opaque-key credential lines (kong-entrypoint.sh does
|
|
698
791
|
// the same with a `sed` after substitution).
|
|
699
792
|
y = y
|
|
@@ -783,10 +876,18 @@ async function reloadPostgrestSchema(ctx, db, rest) {
|
|
|
783
876
|
// Ours first, so every reload — ours and the DDL trigger's — is already
|
|
784
877
|
// queued before we start watching for the result.
|
|
785
878
|
await applySqlFile(ctx, "notify pgrst, 'reload schema';\n", "schema reload", "postgres", db);
|
|
879
|
+
// Only the tables PostgREST will actually publish. The spec below is
|
|
880
|
+
// fetched unauthenticated, so it is the `anon` role's view — and a table
|
|
881
|
+
// deliberately granted to nobody else (Supabase projects do this routinely:
|
|
882
|
+
// a lookup table only `supabase_auth_admin` may read, RLS with no anon
|
|
883
|
+
// grant) never appears in it. Waiting for those would wait for ever, on a
|
|
884
|
+
// schema cache that is in fact up to date.
|
|
786
885
|
const listed = await ctx.exec(db, [
|
|
787
886
|
"psql", "-U", "postgres", "-d", "postgres", "-t", "-A", "-c",
|
|
788
|
-
"select
|
|
789
|
-
"where
|
|
887
|
+
"select c.relname from pg_class c join pg_namespace n on n.oid = c.relnamespace " +
|
|
888
|
+
"where n.nspname = 'public' and c.relkind = 'r' " +
|
|
889
|
+
// `anon` is created by this component's own bootstrap, so it exists.
|
|
890
|
+
"and has_table_privilege('anon', c.oid, 'SELECT') order by 1",
|
|
790
891
|
], { timeoutMs: 60_000 });
|
|
791
892
|
const tables = String(listed.stdout)
|
|
792
893
|
.split("\n")
|
|
@@ -845,42 +946,125 @@ export function supabase(opts = {}) {
|
|
|
845
946
|
if (jwtSecret.length < 32) {
|
|
846
947
|
throw new Error(`supabase(): jwtSecret must be at least 32 characters (got ${jwtSecret.length})`);
|
|
847
948
|
}
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
const withAuth = opts.auth ?? true;
|
|
949
|
+
const withAuth = opts.auth !== false;
|
|
950
|
+
const authOpts = typeof opts.auth === "object" ? opts.auth : {};
|
|
851
951
|
const withStorage = opts.storage ?? true;
|
|
852
952
|
const withRealtime = opts.realtime ?? true;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
953
|
+
// ── the project's supabase/ folder ────────────────────────────────────
|
|
954
|
+
// One option locates everything, because the Supabase CLI fixes the layout:
|
|
955
|
+
// it finds a project by `supabase/config.toml`, `supabase migration new`
|
|
956
|
+
// writes to `supabase/migrations`, and a function is `supabase/functions/
|
|
957
|
+
// <name>/`. So there is nothing to point at piece by piece — name the
|
|
958
|
+
// folder, and each piece inside it is optional.
|
|
959
|
+
const dirPath = opts.dir === undefined ? null : absProjectPath(opts.dir);
|
|
960
|
+
if (dirPath !== null && !existsSync(dirPath)) {
|
|
961
|
+
throw new Error(`supabase(): dir ${JSON.stringify(opts.dir)} does not resolve to a ` +
|
|
962
|
+
`directory (looked at ${dirPath}). It should be the project's Supabase ` +
|
|
963
|
+
`folder — the one holding config.toml / migrations / functions. The ` +
|
|
964
|
+
`path is resolved inside the uploaded project — check it isn't ignored ` +
|
|
965
|
+
`(e.g. via .spectestignore).`);
|
|
966
|
+
}
|
|
967
|
+
const inDir = (rel) => {
|
|
968
|
+
const p = dirPath === null ? null : join(dirPath, rel);
|
|
969
|
+
return p !== null && existsSync(p) ? p : null;
|
|
970
|
+
};
|
|
971
|
+
const fnDir = inDir("functions");
|
|
972
|
+
const withFunctions = fnDir !== null;
|
|
973
|
+
// The functions directory as the runtime will see it: the same path it has
|
|
974
|
+
// in the repo, under the root mount.
|
|
975
|
+
const root = projectRootPath();
|
|
976
|
+
/** The functions directory relative to the build context (the project root). */
|
|
977
|
+
const fnRelPath = fnDir === null ? "" : fnDir.startsWith(`${root}/`) ? fnDir.slice(root.length + 1) : "supabase/functions";
|
|
978
|
+
const fnServePath = fnDir === null
|
|
869
979
|
? ""
|
|
870
|
-
:
|
|
871
|
-
?
|
|
872
|
-
:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
980
|
+
: fnDir.startsWith(`${root}/`)
|
|
981
|
+
? `${REPO_MOUNT}/${fnDir.slice(root.length + 1)}`
|
|
982
|
+
: `${REPO_MOUNT}/supabase/functions`;
|
|
983
|
+
const migrationsPath = inDir("migrations");
|
|
984
|
+
// ── config file (config.toml / config.json) ───────────────────────────
|
|
985
|
+
// The CLI's own project config, mapped onto the stack. Read at build time:
|
|
986
|
+
// the values become container config, so they must be in hand before the
|
|
987
|
+
// services map is returned. `config.json` wins where a newer CLI wrote one.
|
|
988
|
+
const configPath = inDir("config.json") ?? inDir("config.toml");
|
|
989
|
+
let mapped = null;
|
|
990
|
+
if (configPath !== null) {
|
|
991
|
+
const shown = configPath.startsWith(projectRootPath())
|
|
992
|
+
? configPath.slice(projectRootPath().length + 1)
|
|
993
|
+
: configPath;
|
|
994
|
+
mapped = mapSupabaseConfig(parseSupabaseConfigFile(configPath));
|
|
995
|
+
// What the file asked for and did not get. Reported, never fatal: the
|
|
996
|
+
// config belongs to the project and describes more than this environment,
|
|
997
|
+
// so a key that is meaningless in a hermetic VM is not grounds to refuse
|
|
998
|
+
// to start. Silence is the thing to avoid, not the setting itself.
|
|
999
|
+
for (const { key, reason } of mapped.unsupported) {
|
|
1000
|
+
console.warn(`supabase(): ${shown} sets ${key}, which this stack does not apply — ${reason}.`);
|
|
1001
|
+
}
|
|
1002
|
+
const unmet = Object.entries(mapped.deferredSecrets)
|
|
1003
|
+
.filter(([, from]) => opts.env?.[from] === undefined)
|
|
1004
|
+
.map(([name, from]) => (name === from ? name : `${name} (env ${from})`));
|
|
1005
|
+
if (unmet.length > 0) {
|
|
1006
|
+
console.warn(`supabase(): ${shown} reads ${unmet.length} value(s) from the ` +
|
|
1007
|
+
`environment: ${unmet.join(", ")}. They are unset here — pass them ` +
|
|
1008
|
+
`with env: { … } if a test needs them.`);
|
|
1009
|
+
}
|
|
1010
|
+
if (mapped.encryptedValues.length > 0) {
|
|
1011
|
+
console.warn(`supabase(): ${shown} carries encrypted values (${mapped.encryptedValues.join(", ")}), ` +
|
|
1012
|
+
`which need a DOTENV_PRIVATE_KEY this environment does not have. They ` +
|
|
1013
|
+
`are unset here.`);
|
|
1014
|
+
}
|
|
1015
|
+
// The other direction: something supplied here that the config never
|
|
1016
|
+
// reads. Without this the gap is silent — a function goes without a value
|
|
1017
|
+
// and nothing says why.
|
|
1018
|
+
const referenced = new Set([
|
|
1019
|
+
...Object.values(mapped.deferredSecrets),
|
|
1020
|
+
...Object.values(mapped.externalProviders).flatMap((p) => [p.clientIdFrom, p.secretFrom].filter((n) => n !== undefined)),
|
|
1021
|
+
]);
|
|
1022
|
+
const unread = Object.keys(opts.env ?? {}).filter((k) => !referenced.has(k));
|
|
1023
|
+
if (unread.length > 0) {
|
|
1024
|
+
console.warn(`supabase(): env supplies ${unread.join(", ")}, which ${shown} never ` +
|
|
1025
|
+
`reads with env(...) — nothing receives them. Declare what a function ` +
|
|
1026
|
+
`needs in [edge_runtime.secrets], e.g. NAME = "env(NAME)".`);
|
|
1027
|
+
}
|
|
1028
|
+
for (const key of mapped.warnings) {
|
|
1029
|
+
console.warn(`supabase(): ${shown} sets ${key}, which this component does not know about — ignoring it.`);
|
|
1030
|
+
}
|
|
878
1031
|
}
|
|
1032
|
+
// The OAuth server is on only if the project's config says so — it adds
|
|
1033
|
+
// gateway routes, so the stack has to know before Kong's config is built.
|
|
1034
|
+
const withOAuthServer = mapped?.auth.GOTRUE_OAUTH_SERVER_ENABLED === "true";
|
|
879
1035
|
const withMail = opts.mail !== false;
|
|
880
1036
|
const mailOpts = typeof opts.mail === "object" ? opts.mail : {};
|
|
881
1037
|
const mailExternal = mailOpts.service !== undefined;
|
|
882
1038
|
const mailKey = mailOpts.service ?? `${name}-mail`;
|
|
883
|
-
|
|
1039
|
+
// ── custom mail templates ─────────────────────────────────────────────
|
|
1040
|
+
// `[auth.email.template.*]` from the project's config, keyed by GoTrue's own
|
|
1041
|
+
// template ids — which is what the container variables are named after.
|
|
1042
|
+
const templates = { ...mapped?.templates };
|
|
1043
|
+
// A subject with no body is legitimate (GoTrue's default template, the
|
|
1044
|
+
// project's wording), and only a body needs serving.
|
|
1045
|
+
const templateBodies = Object.entries(templates).filter(([, spec]) => spec.path !== "");
|
|
1046
|
+
if (templateBodies.length > 0 && (!withMail || !withAuth)) {
|
|
1047
|
+
const why = !withAuth ? "auth is off (auth: false)" : "mail is off (mail: false)";
|
|
1048
|
+
throw new Error(`supabase(): the config file sets mail templates (${templateBodies
|
|
1049
|
+
.map(([id]) => id)
|
|
1050
|
+
.join(", ")}) but ${why}, so nothing would ever send them.`);
|
|
1051
|
+
}
|
|
1052
|
+
const templateFiles = templateBodies.map(([id, spec]) => {
|
|
1053
|
+
// `content_path` is written relative to the project root, the way the CLI
|
|
1054
|
+
// resolves it — not relative to the supabase/ folder.
|
|
1055
|
+
const path = absProjectPath(spec.path);
|
|
1056
|
+
if (!existsSync(path)) {
|
|
1057
|
+
throw new Error(`supabase(): mail template ${JSON.stringify(spec.path)} for ${id} does ` +
|
|
1058
|
+
`not resolve to a file (looked at ${path}). The path is resolved ` +
|
|
1059
|
+
`inside the uploaded project — check it isn't ignored (e.g. via ` +
|
|
1060
|
+
`.spectestignore).`);
|
|
1061
|
+
}
|
|
1062
|
+
// Read now rather than bind-mounting the directory: a template's config
|
|
1063
|
+
// key decides the served name, which a mount cannot rename, and the paths
|
|
1064
|
+
// may be anywhere in the repo.
|
|
1065
|
+
return { path: `/srv/email/${id}.html`, content: readFileSync(path, "utf8") };
|
|
1066
|
+
});
|
|
1067
|
+
const withTemplates = templateFiles.length > 0;
|
|
884
1068
|
const extraAnonKeys = opts.extraApiKeys?.anon ?? [];
|
|
885
1069
|
const extraServiceRoleKeys = opts.extraApiKeys?.serviceRole ?? [];
|
|
886
1070
|
for (const k of [...extraAnonKeys, ...extraServiceRoleKeys]) {
|
|
@@ -903,6 +1087,34 @@ export function supabase(opts = {}) {
|
|
|
903
1087
|
// `supabase_admin` so it can inspect everything — public tables plus the
|
|
904
1088
|
// `auth`/`storage` schemas — regardless of ownership.
|
|
905
1089
|
const adminDbUrl = `postgresql://supabase_admin:${dbPassword}@${dbKey}:5432/postgres`;
|
|
1090
|
+
// ── social sign-in ────────────────────────────────────────────────────
|
|
1091
|
+
// Every `[auth.external.<name>]` the config enables: served in-environment
|
|
1092
|
+
// where there is an emulator for it, reported where there isn't. The app
|
|
1093
|
+
// under test keeps its production configuration either way — the provider
|
|
1094
|
+
// answers at its own real endpoints.
|
|
1095
|
+
const providerOverrides = authOpts.providers ?? {};
|
|
1096
|
+
const externalProviders = Object.entries(mapped?.externalProviders ?? {})
|
|
1097
|
+
.filter(([, p]) => p.enabled)
|
|
1098
|
+
.map(([providerName, declared]) => {
|
|
1099
|
+
const { clientId, clientSecret } = providerCredentials(providerName, declared, opts.env);
|
|
1100
|
+
// Registering the client makes the provider validate it, so a callback
|
|
1101
|
+
// URL that doesn't match fails here rather than in production.
|
|
1102
|
+
const service = providerOverrides[providerName] ??
|
|
1103
|
+
EXTERNAL_PROVIDERS[providerName]?.({
|
|
1104
|
+
clientId,
|
|
1105
|
+
clientSecret,
|
|
1106
|
+
redirectUris: [`${url}/auth/v1/callback`],
|
|
1107
|
+
});
|
|
1108
|
+
return { providerName, declared, service };
|
|
1109
|
+
});
|
|
1110
|
+
for (const { providerName, service } of externalProviders) {
|
|
1111
|
+
if (service === undefined) {
|
|
1112
|
+
console.warn(`supabase(): the config enables the ${providerName} sign-in provider, which ` +
|
|
1113
|
+
`this stack has no emulator for — sign-in through it will not work here. ` +
|
|
1114
|
+
`Supply one with auth: { providers: { ${providerName}: … } }.`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const servedProviders = externalProviders.filter((p) => p.service !== undefined);
|
|
906
1118
|
const group = serviceGroup({
|
|
907
1119
|
// Kong: the group key itself is the gateway (`http://<name>:8000`), and
|
|
908
1120
|
// as the primary it's the group's sink — `dependsOn: [sb.ready]` from
|
|
@@ -971,6 +1183,9 @@ export function supabase(opts = {}) {
|
|
|
971
1183
|
PGRST_ADMIN_SERVER_PORT: "3001",
|
|
972
1184
|
PGRST_DB_MAX_ROWS: "1000",
|
|
973
1185
|
PGRST_DB_EXTRA_SEARCH_PATH: "public",
|
|
1186
|
+
// `api.schemas` / `extra_search_path` / `max_rows` from the project's
|
|
1187
|
+
// config file, if it has one.
|
|
1188
|
+
...(mapped?.rest ?? {}),
|
|
974
1189
|
},
|
|
975
1190
|
ports: [3000],
|
|
976
1191
|
dependsOn: ["db"],
|
|
@@ -982,9 +1197,39 @@ export function supabase(opts = {}) {
|
|
|
982
1197
|
if (withMail && !mailExternal) {
|
|
983
1198
|
parts.mail = email();
|
|
984
1199
|
}
|
|
1200
|
+
// ── social sign-in providers ────────────────────────────────────
|
|
1201
|
+
for (const { providerName, service } of servedProviders) {
|
|
1202
|
+
parts[providerName] = service;
|
|
1203
|
+
}
|
|
1204
|
+
// ── mail templates (static server) ──────────────────────────────
|
|
1205
|
+
// GoTrue fetches a template over HTTP — `http.DefaultClient.Do` on the
|
|
1206
|
+
// configured URL — so a bind-mounted path or a `file://` URL does not
|
|
1207
|
+
// work. Upstream's CLI solves this with an nginx static server beside
|
|
1208
|
+
// Kong; this is the same shape as its own part, and only exists when a
|
|
1209
|
+
// template is configured.
|
|
1210
|
+
if (withTemplates) {
|
|
1211
|
+
parts.templates = {
|
|
1212
|
+
image: { type: "registry", reference: IMAGES.templates },
|
|
1213
|
+
command: `httpd -f -p ${TEMPLATE_PORT} -h /srv`,
|
|
1214
|
+
files: templateFiles,
|
|
1215
|
+
ports: [TEMPLATE_PORT],
|
|
1216
|
+
readyCheck: { type: "tcp", port: TEMPLATE_PORT, timeoutSecs: 60 },
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
985
1219
|
// ── auth (GoTrue) ───────────────────────────────────────────────
|
|
986
1220
|
if (withAuth) {
|
|
987
1221
|
const apiExternalUrl = `${url}/auth/v1`;
|
|
1222
|
+
const templateHost = `${g.key("templates")}:${TEMPLATE_PORT}`;
|
|
1223
|
+
const templateEnv = {};
|
|
1224
|
+
for (const [id, spec] of Object.entries(templates)) {
|
|
1225
|
+
if (spec.path !== "") {
|
|
1226
|
+
templateEnv[`GOTRUE_MAILER_TEMPLATES_${id.toUpperCase()}`] =
|
|
1227
|
+
`http://${templateHost}/email/${id}.html`;
|
|
1228
|
+
}
|
|
1229
|
+
if (spec.subject !== undefined) {
|
|
1230
|
+
templateEnv[`GOTRUE_MAILER_SUBJECTS_${id.toUpperCase()}`] = spec.subject;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
988
1233
|
parts.auth = {
|
|
989
1234
|
image: { type: "registry", reference: IMAGES.auth },
|
|
990
1235
|
env: {
|
|
@@ -993,8 +1238,13 @@ export function supabase(opts = {}) {
|
|
|
993
1238
|
API_EXTERNAL_URL: apiExternalUrl,
|
|
994
1239
|
GOTRUE_DB_DRIVER: "postgres",
|
|
995
1240
|
GOTRUE_DB_DATABASE_URL: `postgres://supabase_auth_admin:${dbPassword}@${db}:5432/postgres`,
|
|
996
|
-
|
|
997
|
-
|
|
1241
|
+
// Addresses in *this* environment, so they come from the option
|
|
1242
|
+
// rather than the config file (whose values name the developer's
|
|
1243
|
+
// machine). The wildcard is deliberate: GoTrue has no way to turn
|
|
1244
|
+
// redirect validation off, and an empty allow list is its
|
|
1245
|
+
// strictest state, not its loosest.
|
|
1246
|
+
GOTRUE_SITE_URL: authOpts.siteUrl ?? url,
|
|
1247
|
+
GOTRUE_URI_ALLOW_LIST: authOpts.additionalRedirectUrls?.join(",") ?? "*",
|
|
998
1248
|
GOTRUE_DISABLE_SIGNUP: "false",
|
|
999
1249
|
GOTRUE_JWT_ADMIN_ROLES: "service_role",
|
|
1000
1250
|
GOTRUE_JWT_AUD: "authenticated",
|
|
@@ -1013,7 +1263,9 @@ export function supabase(opts = {}) {
|
|
|
1013
1263
|
// Always ON when there is no mailbox (`mail: false`), whatever
|
|
1014
1264
|
// was asked for: the confirmation would be sent nowhere and no
|
|
1015
1265
|
// account could ever be used.
|
|
1016
|
-
|
|
1266
|
+
// One-step signups by default; `[auth.email] enable_confirmations`
|
|
1267
|
+
// in the project's config turns the real confirmation flow on.
|
|
1268
|
+
GOTRUE_MAILER_AUTOCONFIRM: "true",
|
|
1017
1269
|
GOTRUE_EXTERNAL_PHONE_ENABLED: "false",
|
|
1018
1270
|
GOTRUE_SMS_AUTOCONFIRM: "true",
|
|
1019
1271
|
...(withMail
|
|
@@ -1025,8 +1277,8 @@ export function supabase(opts = {}) {
|
|
|
1025
1277
|
// GoTrue's Go smtp.PlainAuth refuses to send them over a
|
|
1026
1278
|
// plaintext connection ("unencrypted connection" → every
|
|
1027
1279
|
// mail 500s). Unauthenticated submission is accepted.
|
|
1028
|
-
GOTRUE_SMTP_ADMIN_EMAIL:
|
|
1029
|
-
GOTRUE_SMTP_SENDER_NAME:
|
|
1280
|
+
GOTRUE_SMTP_ADMIN_EMAIL: "admin@example.com",
|
|
1281
|
+
GOTRUE_SMTP_SENDER_NAME: "Supabase",
|
|
1030
1282
|
// GoTrue rate-limits repeat mail to the same address
|
|
1031
1283
|
// (default 1/min) — far too slow for tests that
|
|
1032
1284
|
// request an OTP, assert, and request again.
|
|
@@ -1037,9 +1289,42 @@ export function supabase(opts = {}) {
|
|
|
1037
1289
|
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: "/auth/v1/verify",
|
|
1038
1290
|
}
|
|
1039
1291
|
: {}),
|
|
1292
|
+
// The project's config file outranks the defaults above — that is
|
|
1293
|
+
// what reading it is for.
|
|
1294
|
+
...(mapped?.auth ?? {}),
|
|
1295
|
+
...templateEnv,
|
|
1296
|
+
// One block per served provider. The credentials are this
|
|
1297
|
+
// environment's own: the config file's are `env(...)` references
|
|
1298
|
+
// to the developer's shell, and nothing here needs the real ones —
|
|
1299
|
+
// only GoTrue and the provider have to agree, and both are ours.
|
|
1300
|
+
...Object.fromEntries(servedProviders.flatMap(({ providerName, declared }) => {
|
|
1301
|
+
const upper = providerName.toUpperCase();
|
|
1302
|
+
const { clientId, clientSecret } = providerCredentials(providerName, declared, opts.env);
|
|
1303
|
+
return [
|
|
1304
|
+
[`GOTRUE_EXTERNAL_${upper}_ENABLED`, "true"],
|
|
1305
|
+
[`GOTRUE_EXTERNAL_${upper}_CLIENT_ID`, clientId],
|
|
1306
|
+
[`GOTRUE_EXTERNAL_${upper}_SECRET`, clientSecret],
|
|
1307
|
+
[`GOTRUE_EXTERNAL_${upper}_REDIRECT_URI`, `${url}/auth/v1/callback`],
|
|
1308
|
+
...(declared.url === undefined
|
|
1309
|
+
? []
|
|
1310
|
+
: [[`GOTRUE_EXTERNAL_${upper}_URL`, declared.url]]),
|
|
1311
|
+
];
|
|
1312
|
+
})),
|
|
1313
|
+
// Autoconfirm is forced on when there is no mailbox, whatever the
|
|
1314
|
+
// config said: the confirmation would go nowhere, and no account
|
|
1315
|
+
// could ever be used.
|
|
1316
|
+
...(!withMail ? { GOTRUE_MAILER_AUTOCONFIRM: "true" } : {}),
|
|
1040
1317
|
},
|
|
1041
1318
|
ports: [9999],
|
|
1042
|
-
dependsOn: [
|
|
1319
|
+
dependsOn: [
|
|
1320
|
+
"db",
|
|
1321
|
+
...(withMail && !mailExternal ? ["mail"] : []),
|
|
1322
|
+
...(withTemplates ? ["templates"] : []),
|
|
1323
|
+
// GoTrue reads the provider's discovery document during a flow,
|
|
1324
|
+
// not at boot — but a test that signs in immediately should not
|
|
1325
|
+
// race the emulator's first start.
|
|
1326
|
+
...servedProviders.map((p) => p.providerName),
|
|
1327
|
+
],
|
|
1043
1328
|
readyCheck: { type: "http", port: 9999, path: "/health", timeoutSecs: 90 },
|
|
1044
1329
|
};
|
|
1045
1330
|
}
|
|
@@ -1132,19 +1417,45 @@ export function supabase(opts = {}) {
|
|
|
1132
1417
|
// ── functions (edge runtime) ────────────────────────────────────
|
|
1133
1418
|
if (withFunctions) {
|
|
1134
1419
|
parts.functions = {
|
|
1135
|
-
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1420
|
+
// Built rather than pulled, so the functions' remote imports are
|
|
1421
|
+
// fetched by the host builder and cached into the image — see
|
|
1422
|
+
// `functionsDockerfile`.
|
|
1423
|
+
image: {
|
|
1424
|
+
type: "dockerfile",
|
|
1425
|
+
content: functionsDockerfile(fnRelPath),
|
|
1426
|
+
},
|
|
1427
|
+
// The **project root**, mounted with the functions directory in its
|
|
1428
|
+
// real position underneath. Mounting only `supabase/functions`
|
|
1429
|
+
// would be enough for a self-contained function, but a monorepo's
|
|
1430
|
+
// functions routinely import shared code from outside it
|
|
1431
|
+
// (`../../../../packages/…`), and that only resolves if the
|
|
1432
|
+
// directory sits where it sits in the repo. Read-only: the runtime
|
|
1433
|
+
// only reads sources, and a function writing into its own directory
|
|
1434
|
+
// would be writing into the user's checkout.
|
|
1435
|
+
volumes: [{ source: projectRootPath(), target: REPO_MOUNT, readOnly: true }],
|
|
1141
1436
|
// Our router (see FUNCTIONS_MAIN_TS) sits outside that mount, so
|
|
1142
1437
|
// the project never has to carry a `main/` function of ours.
|
|
1143
1438
|
files: [{ path: "/home/deno/main/index.ts", content: FUNCTIONS_MAIN_TS }],
|
|
1144
1439
|
args: ["start", "--main-service", "/home/deno/main", "--port", "9000"],
|
|
1145
1440
|
env: {
|
|
1146
1441
|
JWT_SECRET: jwtSecret,
|
|
1147
|
-
|
|
1442
|
+
// Where the router looks for `<name>/`, inside the repo mount.
|
|
1443
|
+
FUNCTIONS_DIR: fnServePath,
|
|
1444
|
+
// The default a function inherits when the config says nothing:
|
|
1445
|
+
// reject an unauthenticated call, as hosted Supabase does.
|
|
1446
|
+
VERIFY_JWT: "true",
|
|
1447
|
+
// Per-function overrides from the config file's `[functions.<name>]`
|
|
1448
|
+
// blocks. Hosted Supabase verifies per function, so one global flag
|
|
1449
|
+
// makes a project pick a single answer for the whole set.
|
|
1450
|
+
VERIFY_JWT_MAP: JSON.stringify(mapped?.functionVerifyJwt ?? {}),
|
|
1451
|
+
// `[edge_runtime.secrets]`, then the values the file deferred to
|
|
1452
|
+
// the environment.
|
|
1453
|
+
...(mapped?.functionSecrets ?? {}),
|
|
1454
|
+
// …the ones it reads from the environment, under the names the
|
|
1455
|
+
// file gives them…
|
|
1456
|
+
...Object.fromEntries(Object.entries(mapped?.deferredSecrets ?? {})
|
|
1457
|
+
.map(([name, from]) => [name, opts.env?.[from]])
|
|
1458
|
+
.filter(([, v]) => v !== undefined)),
|
|
1148
1459
|
// A function calls back into its own project through the
|
|
1149
1460
|
// gateway, exactly as it does when hosted.
|
|
1150
1461
|
SUPABASE_URL: `http://${g.name}:8000`,
|
|
@@ -1152,7 +1463,6 @@ export function supabase(opts = {}) {
|
|
|
1152
1463
|
SUPABASE_ANON_KEY: anonKey,
|
|
1153
1464
|
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
1154
1465
|
SUPABASE_DB_URL: `postgresql://postgres:${dbPassword}@${db}:5432/postgres`,
|
|
1155
|
-
...(fnOpts.env ?? {}),
|
|
1156
1466
|
},
|
|
1157
1467
|
ports: [9000],
|
|
1158
1468
|
dependsOn: ["db"],
|
|
@@ -1164,56 +1474,6 @@ export function supabase(opts = {}) {
|
|
|
1164
1474
|
},
|
|
1165
1475
|
};
|
|
1166
1476
|
}
|
|
1167
|
-
// ── meta (postgres-meta) ────────────────────────────────────────
|
|
1168
|
-
if (withMeta) {
|
|
1169
|
-
parts.meta = {
|
|
1170
|
-
image: { type: "registry", reference: IMAGES.meta },
|
|
1171
|
-
env: {
|
|
1172
|
-
PG_META_PORT: "8080",
|
|
1173
|
-
PG_META_DB_HOST: db,
|
|
1174
|
-
PG_META_DB_PORT: "5432",
|
|
1175
|
-
PG_META_DB_NAME: "postgres",
|
|
1176
|
-
PG_META_DB_USER: "postgres",
|
|
1177
|
-
PG_META_DB_PASSWORD: dbPassword,
|
|
1178
|
-
CRYPTO_KEY: "your-encryption-key-32-chars-min",
|
|
1179
|
-
},
|
|
1180
|
-
ports: [8080],
|
|
1181
|
-
dependsOn: ["db"],
|
|
1182
|
-
readyCheck: { type: "tcp", port: 8080, timeoutSecs: 90 },
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
// ── studio ──────────────────────────────────────────────────────
|
|
1186
|
-
if (withStudio) {
|
|
1187
|
-
parts.studio = {
|
|
1188
|
-
image: { type: "registry", reference: IMAGES.studio },
|
|
1189
|
-
env: {
|
|
1190
|
-
HOSTNAME: "0.0.0.0",
|
|
1191
|
-
STUDIO_PG_META_URL: `http://${g.key("meta")}:8080`,
|
|
1192
|
-
POSTGRES_PORT: "5432",
|
|
1193
|
-
POSTGRES_HOST: db,
|
|
1194
|
-
POSTGRES_DB: "postgres",
|
|
1195
|
-
POSTGRES_PASSWORD: dbPassword,
|
|
1196
|
-
POSTGRES_USER_READ_WRITE: "postgres",
|
|
1197
|
-
PG_META_CRYPTO_KEY: "your-encryption-key-32-chars-min",
|
|
1198
|
-
PGRST_DB_SCHEMAS: "public,graphql_public",
|
|
1199
|
-
DEFAULT_ORGANIZATION_NAME: "Default Organization",
|
|
1200
|
-
DEFAULT_PROJECT_NAME: "Default Project",
|
|
1201
|
-
SUPABASE_URL: `http://${g.name}:8000`,
|
|
1202
|
-
SUPABASE_PUBLIC_URL: url,
|
|
1203
|
-
SUPABASE_ANON_KEY: anonKey,
|
|
1204
|
-
SUPABASE_SERVICE_KEY: serviceRoleKey,
|
|
1205
|
-
AUTH_JWT_SECRET: jwtSecret,
|
|
1206
|
-
},
|
|
1207
|
-
ports: [3000],
|
|
1208
|
-
dependsOn: ["meta"],
|
|
1209
|
-
readyCheck: {
|
|
1210
|
-
type: "http",
|
|
1211
|
-
port: 3000,
|
|
1212
|
-
path: "/api/platform/profile",
|
|
1213
|
-
timeoutSecs: 180,
|
|
1214
|
-
},
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
1477
|
// ── kong (gateway / primary) — no explicit dependsOn: the group
|
|
1218
1478
|
// expansion makes the primary depend on every member ──────────────
|
|
1219
1479
|
parts.gateway = {
|
|
@@ -1234,11 +1494,10 @@ export function supabase(opts = {}) {
|
|
|
1234
1494
|
content: buildKongYaml(g.key, {
|
|
1235
1495
|
anonKey,
|
|
1236
1496
|
serviceRoleKey,
|
|
1237
|
-
dashboardUsername,
|
|
1238
|
-
dashboardPassword,
|
|
1239
1497
|
extraAnonKeys,
|
|
1240
1498
|
extraServiceRoleKeys,
|
|
1241
1499
|
functions: withFunctions,
|
|
1500
|
+
oauthServer: withOAuthServer,
|
|
1242
1501
|
}),
|
|
1243
1502
|
},
|
|
1244
1503
|
],
|
|
@@ -1253,9 +1512,9 @@ export function supabase(opts = {}) {
|
|
|
1253
1512
|
// dependency sink). Still inside bring-up, so the result is captured
|
|
1254
1513
|
// into the cached environment like everything else.
|
|
1255
1514
|
setup: async (ctx) => {
|
|
1256
|
-
if (
|
|
1515
|
+
if (migrationsPath === null)
|
|
1257
1516
|
return;
|
|
1258
|
-
await applyMigrations(ctx,
|
|
1517
|
+
await applyMigrations(ctx, migrationsPath, dbKey);
|
|
1259
1518
|
await reloadPostgrestSchema(ctx, dbKey, `${name}-rest`);
|
|
1260
1519
|
},
|
|
1261
1520
|
// The group's consolidated handle at `ctx.svc.<name>`.
|