@geekmidas/constructs 10.0.0-alpha.33 → 10.0.0-alpha.35
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/{Endpoint-BojvdQoh.d.mts → Endpoint-aG6oyuhR.d.mts} +2 -2
- package/dist/{Endpoint-BojvdQoh.d.mts.map → Endpoint-aG6oyuhR.d.mts.map} +1 -1
- package/dist/{Endpoint-xNGuv2Fq.d.cts → Endpoint-wV6FcBV0.d.cts} +2 -2
- package/dist/{Endpoint-xNGuv2Fq.d.cts.map → Endpoint-wV6FcBV0.d.cts.map} +1 -1
- package/dist/{EndpointFactory-DyT1yOJL.d.cts → EndpointFactory-CitKd3JQ.d.cts} +2 -2
- package/dist/{EndpointFactory-CaYTrUOO.d.mts.map → EndpointFactory-CitKd3JQ.d.cts.map} +1 -1
- package/dist/{EndpointFactory-CaYTrUOO.d.mts → EndpointFactory-DdMqIfSy.d.mts} +2 -2
- package/dist/{EndpointFactory-DyT1yOJL.d.cts.map → EndpointFactory-DdMqIfSy.d.mts.map} +1 -1
- package/dist/adaptors/aws.d.cts +1 -1
- package/dist/adaptors/aws.d.mts +1 -1
- package/dist/adaptors/hono.d.cts +1 -1
- package/dist/adaptors/hono.d.mts +1 -1
- package/dist/adaptors/testing.d.cts +1 -1
- package/dist/adaptors/testing.d.mts +1 -1
- package/dist/{auth-CJ5dp7T7.d.mts → auth-BVkkvI6F.d.mts} +10 -2
- package/dist/{auth-Bmo1wSQF.d.cts.map → auth-BVkkvI6F.d.mts.map} +1 -1
- package/dist/{auth-Bmo1wSQF.d.cts → auth-DYfF6Vfp.d.cts} +10 -2
- package/dist/{auth-CJ5dp7T7.d.mts.map → auth-DYfF6Vfp.d.cts.map} +1 -1
- package/dist/auth.cjs +66 -3
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +2 -2
- package/dist/auth.d.mts +2 -2
- package/dist/auth.mjs +66 -5
- package/dist/auth.mjs.map +1 -1
- package/dist/endpoints/index.d.cts +2 -2
- package/dist/endpoints/index.d.mts +2 -2
- package/dist/mobile-app.cjs +71 -0
- package/dist/mobile-app.cjs.map +1 -0
- package/dist/mobile-app.d.cts +61 -0
- package/dist/mobile-app.d.cts.map +1 -0
- package/dist/mobile-app.d.mts +61 -0
- package/dist/mobile-app.d.mts.map +1 -0
- package/dist/mobile-app.mjs +70 -0
- package/dist/mobile-app.mjs.map +1 -0
- package/dist/rest-api.d.cts +2 -2
- package/dist/rest-api.d.mts +2 -2
- package/dist/site.cjs +3 -3
- package/dist/site.cjs.map +1 -1
- package/dist/site.mjs +3 -3
- package/dist/site.mjs.map +1 -1
- package/package.json +46 -31
package/dist/auth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { edgeTo } from "./construct-interface.mjs";
|
|
2
|
-
import { canonicalId, environmentCase, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
2
|
+
import { canonicalId, environmentCase, isWebOrigin, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
3
3
|
import { betterAuth } from "better-auth";
|
|
4
4
|
//#region src/auth.ts
|
|
5
5
|
const DEFAULT_BASE_PATH = "/api/auth";
|
|
@@ -188,15 +188,20 @@ var BetterAuth = class {
|
|
|
188
188
|
* built from the same options.
|
|
189
189
|
*/
|
|
190
190
|
async optionsFor(options, as = {}) {
|
|
191
|
-
const { secret, baseUrl, trustedOrigins, cookieDomain } = options.envParser.create((get) => ({
|
|
191
|
+
const { secret, baseUrl, trustedOrigins, cookieDomain, deviceUrl } = options.envParser.create((get) => ({
|
|
192
192
|
secret: get(this.keys.secret).string(),
|
|
193
193
|
baseUrl: get(this.keys.url).string(),
|
|
194
194
|
trustedOrigins: get(this.keys.trustedOrigins).string().default("").transform((value) => value.split(",").map((origin) => origin.trim()).filter(Boolean)),
|
|
195
|
-
cookieDomain: get(this.keys.cookieDomain).string().optional()
|
|
195
|
+
cookieDomain: get(this.keys.cookieDomain).string().optional(),
|
|
196
|
+
deviceUrl: get(provideKey(this.id, "deviceUrl")).string().optional()
|
|
196
197
|
})).parse();
|
|
197
198
|
const tenant = this.config.database;
|
|
198
199
|
const db = await (as.owner && tenant.owner ? tenant.owner : tenant.service).register(options);
|
|
199
200
|
const configured = await this.configured(options);
|
|
201
|
+
const origins = [...trustedOrigins, ...configured.trustedOrigins && Array.isArray(configured.trustedOrigins) ? configured.trustedOrigins : []];
|
|
202
|
+
const plugins = [...configured.plugins ?? []];
|
|
203
|
+
if (origins.some((origin) => !isWebOrigin(origin)) && !plugins.some((plugin) => plugin.id === "expo")) plugins.push(await expoPlugin(this.id));
|
|
204
|
+
if (deviceUrl) for (const plugin of plugins) reachableFromDevice(plugin, deviceUrl);
|
|
200
205
|
return {
|
|
201
206
|
...configured,
|
|
202
207
|
secret,
|
|
@@ -206,7 +211,8 @@ var BetterAuth = class {
|
|
|
206
211
|
db,
|
|
207
212
|
type: "postgres"
|
|
208
213
|
},
|
|
209
|
-
|
|
214
|
+
plugins,
|
|
215
|
+
trustedOrigins: origins,
|
|
210
216
|
advanced: {
|
|
211
217
|
...configured.advanced,
|
|
212
218
|
...cookieDomain ? { crossSubDomainCookies: {
|
|
@@ -218,6 +224,61 @@ var BetterAuth = class {
|
|
|
218
224
|
};
|
|
219
225
|
}
|
|
220
226
|
};
|
|
227
|
+
/** Better Auth's Expo plugin, which a mobile app's sign-in needs. */
|
|
228
|
+
async function expoPlugin(server) {
|
|
229
|
+
try {
|
|
230
|
+
const { expo } = await import("@better-auth/expo");
|
|
231
|
+
return expo();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new ExpoPluginMissing(server, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Links already rewritten — an `options` object is reused across builds. */
|
|
237
|
+
const rewritten = /* @__PURE__ */ new WeakSet();
|
|
238
|
+
/**
|
|
239
|
+
* Build the magic links an app asked for on the address a phone reaches.
|
|
240
|
+
*
|
|
241
|
+
* An app's link carries its scheme as the `callbackURL` (`shop-dev://…`)
|
|
242
|
+
* where a browser's carries a path or an `http(s)` URL — which is how the two
|
|
243
|
+
* are told apart. A browser's link is left alone: it is opened on this
|
|
244
|
+
* machine, where the server's own hostname resolves.
|
|
245
|
+
*
|
|
246
|
+
* Better Auth's magic-link plugin calls `sendMagicLink` through the options
|
|
247
|
+
* object it returns, so wrapping it there is wrapping the call.
|
|
248
|
+
*/
|
|
249
|
+
function reachableFromDevice(plugin, deviceUrl) {
|
|
250
|
+
if (plugin.id !== "magic-link") return;
|
|
251
|
+
const options = plugin.options;
|
|
252
|
+
if (!options?.sendMagicLink || rewritten.has(options)) return;
|
|
253
|
+
rewritten.add(options);
|
|
254
|
+
const send = options.sendMagicLink;
|
|
255
|
+
options.sendMagicLink = (data, ...rest) => send({
|
|
256
|
+
...data,
|
|
257
|
+
url: deviceLink(data.url, deviceUrl)
|
|
258
|
+
}, ...rest);
|
|
259
|
+
}
|
|
260
|
+
/** A magic link moved to the device address when an app is its destination. */
|
|
261
|
+
function deviceLink(url, deviceUrl) {
|
|
262
|
+
try {
|
|
263
|
+
const link = new URL(url);
|
|
264
|
+
const callback = link.searchParams.get("callbackURL") ?? "/";
|
|
265
|
+
if (callback.startsWith("/") || isWebOrigin(callback)) return url;
|
|
266
|
+
return new URL(`${link.pathname}${link.search}`, deviceUrl).toString();
|
|
267
|
+
} catch {
|
|
268
|
+
return url;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** A mobile app depends on this auth server, and `@better-auth/expo` is absent. */
|
|
272
|
+
var ExpoPluginMissing = class extends Error {
|
|
273
|
+
server;
|
|
274
|
+
cause;
|
|
275
|
+
constructor(server, cause) {
|
|
276
|
+
super(`'${server}' is called by a mobile app, and a mobile app signs in through Better Auth's Expo plugin — install it where the auth server runs: pnpm add @better-auth/expo`);
|
|
277
|
+
this.server = server;
|
|
278
|
+
this.cause = cause;
|
|
279
|
+
this.name = "ExpoPluginMissing";
|
|
280
|
+
}
|
|
281
|
+
};
|
|
221
282
|
/** The auth server answered a session check with something other than 200. */
|
|
222
283
|
var SessionCheckFailed = class extends Error {
|
|
223
284
|
authenticator;
|
|
@@ -242,6 +303,6 @@ function idempotent(statements) {
|
|
|
242
303
|
return statements.replace(/\bcreate table "/gi, "create table if not exists \"").replace(/\bcreate (unique )?index "/gi, (_, unique = "") => `create ${unique}index if not exists "`).replace(/\badd column "/gi, "add column if not exists \"");
|
|
243
304
|
}
|
|
244
305
|
//#endregion
|
|
245
|
-
export { BetterAuth, SessionCheckFailed, idempotent };
|
|
306
|
+
export { BetterAuth, ExpoPluginMissing, SessionCheckFailed, deviceLink, idempotent };
|
|
246
307
|
|
|
247
308
|
//# sourceMappingURL=auth.mjs.map
|
package/dist/auth.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * `BetterAuth` — a declared auth server.\n *\n * It is three things at once, which is what makes it the construct that\n * exercises most of the model: a consumer of a database, a producer of an\n * authorizer, and a set of endpoints. This lands the first two.\n *\n * Its tables live in a schema tenant it is *given* — `orders.schema('AuthDb')`\n * — rather than one it invents. The tenant declares the schema and the role;\n * this declares the one thing left, the signing secret, and reads back the keys\n * it needs.\n *\n * :::caution\n * The isolation that arrangement is *for* — the application's own role holding\n * no grant on these tables, so a compromised handler cannot read sessions — does\n * **not hold yet**. No target creates the per-schema role: both connect as the\n * database owner, so today the tenant is a `search_path`, which is a namespace\n * and not a privilege boundary. See \"Roles are declared and never created\" in\n * `docs/design/constructs-outstanding.md`.\n * :::\n *\n * It declares its own surface, so where it answers, who may call it, and the\n * domain its cookies are scoped to all arrive as environment resolved by the\n * target — none of them guessed from a port, and none of them listed by hand in\n * application config.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tenvironmentCase,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type {\n\tService,\n\tServiceRecord,\n\tServiceRegisterOptions,\n} from '@geekmidas/services';\nimport { betterAuth } from 'better-auth';\nimport type { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport {\n\ttype Authenticator,\n\ttype Construct,\n\ttype Consumable,\n\tedgeTo,\n\ttype ServicesOf,\n} from './construct-interface';\n\n/** The server better-auth hands back. */\nexport type AuthServer = ReturnType<typeof betterAuth>;\n\n/** A signed-in session as better-auth hands it back: `{ user, session }`. */\nexport type AuthSession = AuthServer['$Infer']['Session'];\n\n/** Everything better-auth takes, minus what the construct owns. */\nexport type BetterAuthOptions = Omit<\n\tParameters<typeof betterAuth>[0],\n\t'database' | 'secret' | 'basePath' | 'baseURL'\n>;\n\n/**\n * What an `options` callback is handed: the options this construct was\n * registered with, and a client for each construct its `dependsOn` names —\n * keyed by service name, exactly as an endpoint's handler receives them.\n */\nexport type BetterAuthOptionsContext<TUses extends readonly Consumable[]> =\n\tServiceRegisterOptions & {\n\t\tservices: ServiceRecord<[...ServicesOf<TUses>]>;\n\t};\n\nexport interface BetterAuthConfig<\n\tTDatabase extends Consumable,\n\tTUses extends readonly Consumable[] = readonly [],\n> {\n\t/**\n\t * The schema tenant its tables live in.\n\t *\n\t * A construct rather than a URL: the tenant already declares its schema and\n\t * its role, and taking the declaration is what puts the edge in the graph\n\t * instead of a connection string in two places.\n\t */\n\tdatabase: TDatabase;\n\t/**\n\t * The app that serves this auth server, relative to the workspace root —\n\t * `'apps/auth'`.\n\t *\n\t * Required, for the reason `RestApi.path` is. Not to be confused with\n\t * `basePath`, which is where its routes are mounted in a URL.\n\t */\n\tpath: string;\n\t/**\n\t * Where the auth routes are mounted, e.g. `/api/auth`.\n\t *\n\t * Structural — it is part of the URL every client calls, so it cannot differ\n\t * between stages the way the host can.\n\t */\n\tbasePath?: string;\n\t/**\n\t * The constructs `options` uses — the mailer a magic link goes through,\n\t * most often.\n\t *\n\t * One list, two jobs, the way an endpoint's `.dependsOn()` has: each is an\n\t * edge on the server's handler, so whatever is composed from the edges (a\n\t * container's environment, a deploy's grants) includes it; and each is\n\t * handed to `options` as a client in `services`, so there is no other way\n\t * for `options` to reach it — nothing to import and register by hand, and\n\t * so nothing that can be used without being declared.\n\t */\n\tdependsOn?: TUses;\n\t/**\n\t * The rest of better-auth's options: providers, plugins, email settings.\n\t *\n\t * A function when they need another construct — a magic-link plugin has to\n\t * *send* the link. It is handed the options this construct was registered\n\t * with, and `services`: a client for each construct in `dependsOn`.\n\t */\n\toptions?:\n\t\t| BetterAuthOptions\n\t\t| ((\n\t\t\t\tcontext: BetterAuthOptionsContext<TUses>,\n\t\t ) => BetterAuthOptions | Promise<BetterAuthOptions>);\n}\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\nexport class BetterAuth<\n\tTName extends string = string,\n\tTDatabase extends Consumable = Consumable,\n\tconst TUses extends readonly Consumable[] = readonly [],\n> implements Construct<TName, AuthServer>, Authenticator<AuthSession>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, AuthServer>;\n\treadonly basePath: string;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the server reads cannot drift.\n\t */\n\tprivate readonly keys: {\n\t\tsecretId: string;\n\t\tsecret: string;\n\t\turl: string;\n\t\ttrustedOrigins: string;\n\t\tcookieDomain: string;\n\t};\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: BetterAuthConfig<TDatabase, TUses>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.basePath = config.basePath ?? DEFAULT_BASE_PATH;\n\n\t\t// A secret's name is its key: `Auth` signs with `AUTH_SECRET`, which is\n\t\t// also what better-auth's own tooling looks for.\n\t\tconst secretId = `${canonical}Secret`;\n\t\tthis.keys = {\n\t\t\tsecretId,\n\t\t\tsecret: environmentCase(secretId),\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\ttrustedOrigins: provideKey(canonical, 'trustedOrigins'),\n\t\t\tcookieDomain: provideKey(canonical, 'cookieDomain'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\t/**\n\t * A signing secret and a surface.\n\t *\n\t * The database is *not* declared here — the tenant that was passed in\n\t * declares it, and declaring it twice is the duplication the whole model\n\t * removes. What is declared is what this construct owns: the key it signs\n\t * with, and the routes it answers on.\n\t *\n\t * One endpoint, wildcarded. Better Auth routes internally and the set of\n\t * paths depends on which capabilities are enabled, so enumerating them here\n\t * would be a copy of its router that goes stale on its next release. The\n\t * surface's job is to send everything under `basePath` to one handler, which\n\t * is exactly what the declaration says.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'secret',\n\t\t\t\tid: this.keys.secretId,\n\t\t\t\tprovides: [this.keys.secret],\n\t\t\t},\n\t\t\t{\n\t\t\t\tkind: 'rest-api',\n\t\t\t\tid: this.id,\n\t\t\t\tpath: this.config.path,\n\t\t\t\tprovides: [\n\t\t\t\t\tthis.keys.url,\n\t\t\t\t\tthis.keys.trustedOrigins,\n\t\t\t\t\tthis.keys.cookieDomain,\n\t\t\t\t],\n\t\t\t\tendpoints: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: `${this.id}Handler`,\n\t\t\t\t\t\thandler: `${this.id}.handler`,\n\t\t\t\t\t\tmethod: 'ANY',\n\t\t\t\t\t\tpath: `${this.basePath}/*`,\n\t\t\t\t\t\t// Read off the tenant rather than written down: this\n\t\t\t\t\t\t// construct takes whatever database it is given, and a\n\t\t\t\t\t\t// hardcoded kind is a second statement of a fact the\n\t\t\t\t\t\t// tenant already makes.\n\t\t\t\t\t\tdependencies: [\n\t\t\t\t\t\t\tedgeTo(this.config.database),\n\t\t\t\t\t\t\t...(this.config.dependsOn ?? []).map(edgeTo),\n\t\t\t\t\t\t],\n\t\t\t\t\t\trequires: [this.keys.secret],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * Whose request this is: the session for these headers, or `null`.\n\t *\n\t * Asked of the auth server over HTTP, at the URL the `.auth()` edge injects —\n\t * so it is the same call whether the server runs in this process, beside\n\t * it, or behind MSW in a test. Only the session headers are forwarded; the\n\t * surface decides which.\n\t */\n\tasync verify(\n\t\theaders: Headers,\n\t\tenvParser: EnvironmentParser<{}>,\n\t): Promise<AuthSession | null> {\n\t\tconst { url } = envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\n\t\tconst response = await fetch(`${url}${this.basePath}/get-session`, {\n\t\t\theaders,\n\t\t});\n\n\t\t// Better Auth answers `null` for a request with no session. Anything\n\t\t// other than a 200 is the server failing, which is not the same thing as\n\t\t// \"signed out\" and must not be read as it.\n\t\tif (!response.ok) {\n\t\t\tthrow new SessionCheckFailed(this.id, response.status);\n\t\t}\n\n\t\treturn (await response.json()) as AuthSession | null;\n\t}\n\n\t/**\n\t * The tenant this server keeps its tables in — whose migrations folder,\n\t * `db/<tenant>/`, Better Auth's schema is written into.\n\t */\n\tget databaseId(): string {\n\t\treturn this.config.database.id;\n\t}\n\n\t/**\n\t * The SQL that brings the tenant up to Better Auth's schema, or `undefined`\n\t * when it already matches.\n\t *\n\t * What `gkm migration` writes as the tenant's next migration, so Better\n\t * Auth's tables are committed, reviewed and applied like any other — rather\n\t * than diffed against the database at runtime, where a change nobody wrote\n\t * down happens on whichever machine migrates first.\n\t *\n\t * Compared as the owner, against the tenant `gkm migrate` has already brought\n\t * up to date: what comes back is only what the committed files are missing.\n\t */\n\tasync pendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined> {\n\t\t// The options, not a server built from them: better-auth checks its\n\t\t// schema when a server starts, and before the migration that creates the\n\t\t// tables it can only report them missing.\n\t\tconst resolved = await this.optionsFor(options, { owner: true });\n\t\tconst db = (resolved.database as { db: Kysely<Record<string, never>> }).db;\n\n\t\ttry {\n\t\t\t// better-auth 1.7 split the migration builder out of `better-auth/db`\n\t\t\t// into its own entry, so importing it no longer drags the whole db\n\t\t\t// layer in behind it.\n\t\t\tconst { getMigrations } = await import('better-auth/db/migration');\n\t\t\tconst { toBeCreated, toBeAdded, toBeAddedIndexes, compileMigrations } =\n\t\t\t\tawait getMigrations(resolved);\n\n\t\t\tif (\n\t\t\t\ttoBeCreated.length === 0 &&\n\t\t\t\ttoBeAdded.length === 0 &&\n\t\t\t\ttoBeAddedIndexes.length === 0\n\t\t\t) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn idempotent(await compileMigrations());\n\t\t} finally {\n\t\t\t// Its own pool: left open, it keeps the process that asked alive.\n\t\t\tawait db.destroy();\n\t\t}\n\t}\n\n\t/**\n\t * The server this surface runs as, built from its own declaration.\n\t *\n\t * A surface is a deploy unit, so something has to serve it, and the thing\n\t * that knows how is the construct — not a generator writing a mount into\n\t * somebody else's entry point. That is how the auth server came to be\n\t * mounted by a hook in the application it was supposed to be separate from,\n\t * and then to be served by nothing at all when the hook was deleted.\n\t *\n\t * The routes come from {@link declare}: one wildcard under `basePath`, read\n\t * off the same declaration the manifest carries, so the surface the graph\n\t * describes and the surface that answers requests cannot disagree.\n\t *\n\t * ```ts\n\t * // .gkm/server/app.ts — the whole generated entry\n\t * import { auth } from '@acme/constructs/auth.js';\n\t * export const { app } = await auth.server({ envParser });\n\t * ```\n\t */\n\tasync server(options: ServiceRegisterOptions): Promise<{ app: Hono }> {\n\t\tconst { Hono } = await import('hono');\n\t\tconst { cors } = await import('hono/cors');\n\t\tconst app = new Hono();\n\t\tconst server = await this.connect(options);\n\n\t\t// Who may call this surface, read off the graph. The same list Better\n\t\t// Auth checks for CSRF — it arrives as `<ID>_TRUSTED_ORIGINS`, composed\n\t\t// by the target from whatever declared an edge to this construct, so a\n\t\t// site that depends on the auth server is allowed to reach it and\n\t\t// nothing else is. Nobody writes an origin down.\n\t\tconst { origins } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value: string) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tapp.use('*', cors({ origin: origins, credentials: true, maxAge: 3600 }));\n\n\t\t// Read off the declaration rather than restated here. `ANY` is every\n\t\t// method Better Auth routes internally, which is the reason the\n\t\t// declaration wildcards rather than enumerating.\n\t\tconst [surface] = this.declare().filter(\n\t\t\t(d): d is Extract<Declaration, { kind: 'rest-api' }> =>\n\t\t\t\td.kind === 'rest-api',\n\t\t);\n\n\t\tfor (const endpoint of surface?.endpoints ?? []) {\n\t\t\tconst methods =\n\t\t\t\tendpoint.method === 'ANY'\n\t\t\t\t\t? (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const)\n\t\t\t\t\t: ([endpoint.method] as const);\n\n\t\t\tapp.on([...methods], endpoint.path, (c) => server.handler(c.req.raw));\n\t\t}\n\n\t\treturn { app };\n\t}\n\n\t/**\n\t * The ids of the better-auth plugins this server runs — what a client has\n\t * to pair its own plugins with (`magic-link` with `magicLinkClient`).\n\t * Resolved the way the server resolves them, options callback included.\n\t */\n\tasync pluginIds(options: ServiceRegisterOptions): Promise<string[]> {\n\t\tconst { plugins = [] } = await this.configured(options);\n\t\treturn plugins.map(({ id }) => id);\n\t}\n\n\t/** Better-auth's options as given — a value, or a callback of the environment. */\n\tprivate async configured(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<BetterAuthOptions> {\n\t\t// Hoisted: narrowing a property of `this` is not preserved across the\n\t\t// await, and the false branch is the plain-object form.\n\t\tconst configure = this.config.options;\n\t\tif (typeof configure !== 'function') return configure ?? {};\n\n\t\t// A client for each declared construct, from the options this call was\n\t\t// given — not the process-wide discovery, which keeps whichever env\n\t\t// parser reached it first.\n\t\tconst services: Record<string, unknown> = {};\n\t\tfor (const construct of this.config.dependsOn ?? []) {\n\t\t\tservices[construct.service.serviceName] =\n\t\t\t\tawait construct.service.register(options);\n\t\t}\n\n\t\treturn configure({\n\t\t\t...options,\n\t\t\tservices,\n\t\t} as BetterAuthOptionsContext<TUses>);\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<AuthServer> {\n\t\t// `Auth<O>` is invariant in its options in better-auth 1.7, so the value\n\t\t// built from a concrete literal is not assignable to the `AuthServer`\n\t\t// alias, which names the constraint. The runtime object is the same one\n\t\t// either way; the assertion keeps the variance where it happens instead\n\t\t// of spreading a generic through the construct's public type.\n\t\treturn betterAuth(await this.optionsFor(options, as)) as AuthServer;\n\t}\n\n\t/**\n\t * Everything better-auth is given: the app's options, and what the construct\n\t * owns — its secret, its URL, its tenant's connection, the origins and cookie\n\t * domain its graph derives. One place, so the server and its migrations are\n\t * built from the same options.\n\t */\n\tprivate async optionsFor(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<Parameters<typeof betterAuth>[0]> {\n\t\tconst { secret, baseUrl, trustedOrigins, cookieDomain } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\tsecret: get(this.keys.secret).string(),\n\t\t\t\t// The surface's own URL, resolved by the target — not guessed from\n\t\t\t\t// a port, which was wrong the moment the server moved to 3001.\n\t\t\t\tbaseUrl: get(this.keys.url).string(),\n\t\t\t\t// Better Auth's CSRF check applies to every caller, not just\n\t\t\t\t// browsers, so an API calling the auth server is rejected unless\n\t\t\t\t// its origin is trusted. The list is derived from the graph and\n\t\t\t\t// arrives as one comma-separated value, for the same reason every\n\t\t\t\t// other derived value arrives as a string: it crosses a process\n\t\t\t\t// boundary as env.\n\t\t\t\ttrustedOrigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t\t// The domain a session cookie has to carry to be readable by a\n\t\t\t\t// frontend on a sibling host. Optional because there is often\n\t\t\t\t// nothing to widen to: locally everything shares `localhost`,\n\t\t\t\t// where cookies ignore the port and a `Domain` would only be a\n\t\t\t\t// value the browser refuses.\n\t\t\t\tcookieDomain: get(this.keys.cookieDomain).string().optional(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\t// The tenant's own client: one construct, one connection, so what auth\n\t\t// writes and what the browser inspects cannot be two different databases.\n\t\t// A migration asks the tenant for its owner connection, which the tenant\n\t\t// exposes as a `Service` and not as anything `.dependsOn()` would take.\n\t\tconst tenant = this.config.database as Consumable & {\n\t\t\towner?: Service<string, unknown>;\n\t\t};\n\t\tconst source = as.owner && tenant.owner ? tenant.owner : tenant.service;\n\n\t\tconst db = (await source.register(options)) as Kysely<\n\t\t\tRecord<string, never>\n\t\t>;\n\n\t\tconst configured = await this.configured(options);\n\n\t\treturn {\n\t\t\t...configured,\n\t\t\tsecret,\n\t\t\tbaseURL: baseUrl,\n\t\t\tbasePath: this.basePath,\n\t\t\tdatabase: { db, type: 'postgres' },\n\t\t\t// Whatever the app added wins over the derived list rather than\n\t\t\t// replacing it: an origin nobody declared is still sometimes real.\n\t\t\ttrustedOrigins: [\n\t\t\t\t...trustedOrigins,\n\t\t\t\t...(configured.trustedOrigins &&\n\t\t\t\tArray.isArray(configured.trustedOrigins)\n\t\t\t\t\t? configured.trustedOrigins\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t\tadvanced: {\n\t\t\t\t...configured.advanced,\n\t\t\t\t// Only when a domain was derived. Better Auth reads the presence\n\t\t\t\t// of this block as intent, so enabling it with no domain would\n\t\t\t\t// widen the cookie to whatever host happened to set it — and the\n\t\t\t\t// case with nothing to widen to is the common one, not an error.\n\t\t\t\t...(cookieDomain\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tcrossSubDomainCookies: {\n\t\t\t\t\t\t\t\tenabled: true,\n\t\t\t\t\t\t\t\tdomain: cookieDomain,\n\t\t\t\t\t\t\t\t...configured.advanced?.crossSubDomainCookies,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** The auth server answered a session check with something other than 200. */\nexport class SessionCheckFailed extends Error {\n\tconstructor(\n\t\treadonly authenticator: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`'${authenticator}' answered a session check with ${status}. ` +\n\t\t\t\t`A request with no session gets 200 and null; this is the server ` +\n\t\t\t\t`failing — check that it is running and reachable at its URL.`,\n\t\t);\n\t\tthis.name = 'SessionCheckFailed';\n\t}\n}\n\n/**\n * Better Auth's DDL, safe to run where its tables already exist.\n *\n * Its SQL is Kysely's, so its shape is known: `create table \"…\"`, `create index\n * \"…\"`, `alter table \"…\" add column \"…\"`. A database that has been running\n * already has those — Better Auth used to create them at runtime — and the\n * first committed migration would fail on them rather than recording that they\n * are there.\n */\nexport function idempotent(statements: string): string {\n\treturn statements\n\t\t.replace(/\\bcreate table \"/gi, 'create table if not exists \"')\n\t\t.replace(\n\t\t\t/\\bcreate (unique )?index \"/gi,\n\t\t\t(_, unique = '') => `create ${unique}index if not exists \"`,\n\t\t)\n\t\t.replace(/\\badd column \"/gi, 'add column if not exists \"');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * `BetterAuth` — a declared auth server.\n *\n * It is three things at once, which is what makes it the construct that\n * exercises most of the model: a consumer of a database, a producer of an\n * authorizer, and a set of endpoints. This lands the first two.\n *\n * Its tables live in a schema tenant it is *given* — `orders.schema('AuthDb')`\n * — rather than one it invents. The tenant declares the schema and the role;\n * this declares the one thing left, the signing secret, and reads back the keys\n * it needs.\n *\n * :::caution\n * The isolation that arrangement is *for* — the application's own role holding\n * no grant on these tables, so a compromised handler cannot read sessions — does\n * **not hold yet**. No target creates the per-schema role: both connect as the\n * database owner, so today the tenant is a `search_path`, which is a namespace\n * and not a privilege boundary. See \"Roles are declared and never created\" in\n * `docs/design/constructs-outstanding.md`.\n * :::\n *\n * It declares its own surface, so where it answers, who may call it, and the\n * domain its cookies are scoped to all arrive as environment resolved by the\n * target — none of them guessed from a port, and none of them listed by hand in\n * application config.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tenvironmentCase,\n\tisWebOrigin,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type {\n\tService,\n\tServiceRecord,\n\tServiceRegisterOptions,\n} from '@geekmidas/services';\nimport { betterAuth } from 'better-auth';\nimport type { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport {\n\ttype Authenticator,\n\ttype Construct,\n\ttype Consumable,\n\tedgeTo,\n\ttype ServicesOf,\n} from './construct-interface';\n\n/** The server better-auth hands back. */\nexport type AuthServer = ReturnType<typeof betterAuth>;\n\n/** A signed-in session as better-auth hands it back: `{ user, session }`. */\nexport type AuthSession = AuthServer['$Infer']['Session'];\n\n/** Everything better-auth takes, minus what the construct owns. */\nexport type BetterAuthOptions = Omit<\n\tParameters<typeof betterAuth>[0],\n\t'database' | 'secret' | 'basePath' | 'baseURL'\n>;\n\n/**\n * What an `options` callback is handed: the options this construct was\n * registered with, and a client for each construct its `dependsOn` names —\n * keyed by service name, exactly as an endpoint's handler receives them.\n */\nexport type BetterAuthOptionsContext<TUses extends readonly Consumable[]> =\n\tServiceRegisterOptions & {\n\t\tservices: ServiceRecord<[...ServicesOf<TUses>]>;\n\t};\n\nexport interface BetterAuthConfig<\n\tTDatabase extends Consumable,\n\tTUses extends readonly Consumable[] = readonly [],\n> {\n\t/**\n\t * The schema tenant its tables live in.\n\t *\n\t * A construct rather than a URL: the tenant already declares its schema and\n\t * its role, and taking the declaration is what puts the edge in the graph\n\t * instead of a connection string in two places.\n\t */\n\tdatabase: TDatabase;\n\t/**\n\t * The app that serves this auth server, relative to the workspace root —\n\t * `'apps/auth'`.\n\t *\n\t * Required, for the reason `RestApi.path` is. Not to be confused with\n\t * `basePath`, which is where its routes are mounted in a URL.\n\t */\n\tpath: string;\n\t/**\n\t * Where the auth routes are mounted, e.g. `/api/auth`.\n\t *\n\t * Structural — it is part of the URL every client calls, so it cannot differ\n\t * between stages the way the host can.\n\t */\n\tbasePath?: string;\n\t/**\n\t * The constructs `options` uses — the mailer a magic link goes through,\n\t * most often.\n\t *\n\t * One list, two jobs, the way an endpoint's `.dependsOn()` has: each is an\n\t * edge on the server's handler, so whatever is composed from the edges (a\n\t * container's environment, a deploy's grants) includes it; and each is\n\t * handed to `options` as a client in `services`, so there is no other way\n\t * for `options` to reach it — nothing to import and register by hand, and\n\t * so nothing that can be used without being declared.\n\t */\n\tdependsOn?: TUses;\n\t/**\n\t * The rest of better-auth's options: providers, plugins, email settings.\n\t *\n\t * A function when they need another construct — a magic-link plugin has to\n\t * *send* the link. It is handed the options this construct was registered\n\t * with, and `services`: a client for each construct in `dependsOn`.\n\t */\n\toptions?:\n\t\t| BetterAuthOptions\n\t\t| ((\n\t\t\t\tcontext: BetterAuthOptionsContext<TUses>,\n\t\t ) => BetterAuthOptions | Promise<BetterAuthOptions>);\n}\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\nexport class BetterAuth<\n\tTName extends string = string,\n\tTDatabase extends Consumable = Consumable,\n\tconst TUses extends readonly Consumable[] = readonly [],\n> implements Construct<TName, AuthServer>, Authenticator<AuthSession>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, AuthServer>;\n\treadonly basePath: string;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the server reads cannot drift.\n\t */\n\tprivate readonly keys: {\n\t\tsecretId: string;\n\t\tsecret: string;\n\t\turl: string;\n\t\ttrustedOrigins: string;\n\t\tcookieDomain: string;\n\t};\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: BetterAuthConfig<TDatabase, TUses>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.basePath = config.basePath ?? DEFAULT_BASE_PATH;\n\n\t\t// A secret's name is its key: `Auth` signs with `AUTH_SECRET`, which is\n\t\t// also what better-auth's own tooling looks for.\n\t\tconst secretId = `${canonical}Secret`;\n\t\tthis.keys = {\n\t\t\tsecretId,\n\t\t\tsecret: environmentCase(secretId),\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\ttrustedOrigins: provideKey(canonical, 'trustedOrigins'),\n\t\t\tcookieDomain: provideKey(canonical, 'cookieDomain'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\t/**\n\t * A signing secret and a surface.\n\t *\n\t * The database is *not* declared here — the tenant that was passed in\n\t * declares it, and declaring it twice is the duplication the whole model\n\t * removes. What is declared is what this construct owns: the key it signs\n\t * with, and the routes it answers on.\n\t *\n\t * One endpoint, wildcarded. Better Auth routes internally and the set of\n\t * paths depends on which capabilities are enabled, so enumerating them here\n\t * would be a copy of its router that goes stale on its next release. The\n\t * surface's job is to send everything under `basePath` to one handler, which\n\t * is exactly what the declaration says.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'secret',\n\t\t\t\tid: this.keys.secretId,\n\t\t\t\tprovides: [this.keys.secret],\n\t\t\t},\n\t\t\t{\n\t\t\t\tkind: 'rest-api',\n\t\t\t\tid: this.id,\n\t\t\t\tpath: this.config.path,\n\t\t\t\tprovides: [\n\t\t\t\t\tthis.keys.url,\n\t\t\t\t\tthis.keys.trustedOrigins,\n\t\t\t\t\tthis.keys.cookieDomain,\n\t\t\t\t],\n\t\t\t\tendpoints: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: `${this.id}Handler`,\n\t\t\t\t\t\thandler: `${this.id}.handler`,\n\t\t\t\t\t\tmethod: 'ANY',\n\t\t\t\t\t\tpath: `${this.basePath}/*`,\n\t\t\t\t\t\t// Read off the tenant rather than written down: this\n\t\t\t\t\t\t// construct takes whatever database it is given, and a\n\t\t\t\t\t\t// hardcoded kind is a second statement of a fact the\n\t\t\t\t\t\t// tenant already makes.\n\t\t\t\t\t\tdependencies: [\n\t\t\t\t\t\t\tedgeTo(this.config.database),\n\t\t\t\t\t\t\t...(this.config.dependsOn ?? []).map(edgeTo),\n\t\t\t\t\t\t],\n\t\t\t\t\t\trequires: [this.keys.secret],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * Whose request this is: the session for these headers, or `null`.\n\t *\n\t * Asked of the auth server over HTTP, at the URL the `.auth()` edge injects —\n\t * so it is the same call whether the server runs in this process, beside\n\t * it, or behind MSW in a test. Only the session headers are forwarded; the\n\t * surface decides which.\n\t */\n\tasync verify(\n\t\theaders: Headers,\n\t\tenvParser: EnvironmentParser<{}>,\n\t): Promise<AuthSession | null> {\n\t\tconst { url } = envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\n\t\tconst response = await fetch(`${url}${this.basePath}/get-session`, {\n\t\t\theaders,\n\t\t});\n\n\t\t// Better Auth answers `null` for a request with no session. Anything\n\t\t// other than a 200 is the server failing, which is not the same thing as\n\t\t// \"signed out\" and must not be read as it.\n\t\tif (!response.ok) {\n\t\t\tthrow new SessionCheckFailed(this.id, response.status);\n\t\t}\n\n\t\treturn (await response.json()) as AuthSession | null;\n\t}\n\n\t/**\n\t * The tenant this server keeps its tables in — whose migrations folder,\n\t * `db/<tenant>/`, Better Auth's schema is written into.\n\t */\n\tget databaseId(): string {\n\t\treturn this.config.database.id;\n\t}\n\n\t/**\n\t * The SQL that brings the tenant up to Better Auth's schema, or `undefined`\n\t * when it already matches.\n\t *\n\t * What `gkm migration` writes as the tenant's next migration, so Better\n\t * Auth's tables are committed, reviewed and applied like any other — rather\n\t * than diffed against the database at runtime, where a change nobody wrote\n\t * down happens on whichever machine migrates first.\n\t *\n\t * Compared as the owner, against the tenant `gkm migrate` has already brought\n\t * up to date: what comes back is only what the committed files are missing.\n\t */\n\tasync pendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined> {\n\t\t// The options, not a server built from them: better-auth checks its\n\t\t// schema when a server starts, and before the migration that creates the\n\t\t// tables it can only report them missing.\n\t\tconst resolved = await this.optionsFor(options, { owner: true });\n\t\tconst db = (resolved.database as { db: Kysely<Record<string, never>> }).db;\n\n\t\ttry {\n\t\t\t// better-auth 1.7 split the migration builder out of `better-auth/db`\n\t\t\t// into its own entry, so importing it no longer drags the whole db\n\t\t\t// layer in behind it.\n\t\t\tconst { getMigrations } = await import('better-auth/db/migration');\n\t\t\tconst { toBeCreated, toBeAdded, toBeAddedIndexes, compileMigrations } =\n\t\t\t\tawait getMigrations(resolved);\n\n\t\t\tif (\n\t\t\t\ttoBeCreated.length === 0 &&\n\t\t\t\ttoBeAdded.length === 0 &&\n\t\t\t\ttoBeAddedIndexes.length === 0\n\t\t\t) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn idempotent(await compileMigrations());\n\t\t} finally {\n\t\t\t// Its own pool: left open, it keeps the process that asked alive.\n\t\t\tawait db.destroy();\n\t\t}\n\t}\n\n\t/**\n\t * The server this surface runs as, built from its own declaration.\n\t *\n\t * A surface is a deploy unit, so something has to serve it, and the thing\n\t * that knows how is the construct — not a generator writing a mount into\n\t * somebody else's entry point. That is how the auth server came to be\n\t * mounted by a hook in the application it was supposed to be separate from,\n\t * and then to be served by nothing at all when the hook was deleted.\n\t *\n\t * The routes come from {@link declare}: one wildcard under `basePath`, read\n\t * off the same declaration the manifest carries, so the surface the graph\n\t * describes and the surface that answers requests cannot disagree.\n\t *\n\t * ```ts\n\t * // .gkm/server/app.ts — the whole generated entry\n\t * import { auth } from '@acme/constructs/auth.js';\n\t * export const { app } = await auth.server({ envParser });\n\t * ```\n\t */\n\tasync server(options: ServiceRegisterOptions): Promise<{ app: Hono }> {\n\t\tconst { Hono } = await import('hono');\n\t\tconst { cors } = await import('hono/cors');\n\t\tconst app = new Hono();\n\t\tconst server = await this.connect(options);\n\n\t\t// Who may call this surface, read off the graph. The same list Better\n\t\t// Auth checks for CSRF — it arrives as `<ID>_TRUSTED_ORIGINS`, composed\n\t\t// by the target from whatever declared an edge to this construct, so a\n\t\t// site that depends on the auth server is allowed to reach it and\n\t\t// nothing else is. Nobody writes an origin down.\n\t\tconst { origins } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value: string) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tapp.use('*', cors({ origin: origins, credentials: true, maxAge: 3600 }));\n\n\t\t// Read off the declaration rather than restated here. `ANY` is every\n\t\t// method Better Auth routes internally, which is the reason the\n\t\t// declaration wildcards rather than enumerating.\n\t\tconst [surface] = this.declare().filter(\n\t\t\t(d): d is Extract<Declaration, { kind: 'rest-api' }> =>\n\t\t\t\td.kind === 'rest-api',\n\t\t);\n\n\t\tfor (const endpoint of surface?.endpoints ?? []) {\n\t\t\tconst methods =\n\t\t\t\tendpoint.method === 'ANY'\n\t\t\t\t\t? (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const)\n\t\t\t\t\t: ([endpoint.method] as const);\n\n\t\t\tapp.on([...methods], endpoint.path, (c) => server.handler(c.req.raw));\n\t\t}\n\n\t\treturn { app };\n\t}\n\n\t/**\n\t * The ids of the better-auth plugins this server runs — what a client has\n\t * to pair its own plugins with (`magic-link` with `magicLinkClient`).\n\t * Resolved the way the server resolves them, options callback included.\n\t */\n\tasync pluginIds(options: ServiceRegisterOptions): Promise<string[]> {\n\t\tconst { plugins = [] } = await this.configured(options);\n\t\treturn plugins.map(({ id }) => id);\n\t}\n\n\t/** Better-auth's options as given — a value, or a callback of the environment. */\n\tprivate async configured(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<BetterAuthOptions> {\n\t\t// Hoisted: narrowing a property of `this` is not preserved across the\n\t\t// await, and the false branch is the plain-object form.\n\t\tconst configure = this.config.options;\n\t\tif (typeof configure !== 'function') return configure ?? {};\n\n\t\t// A client for each declared construct, from the options this call was\n\t\t// given — not the process-wide discovery, which keeps whichever env\n\t\t// parser reached it first.\n\t\tconst services: Record<string, unknown> = {};\n\t\tfor (const construct of this.config.dependsOn ?? []) {\n\t\t\tservices[construct.service.serviceName] =\n\t\t\t\tawait construct.service.register(options);\n\t\t}\n\n\t\treturn configure({\n\t\t\t...options,\n\t\t\tservices,\n\t\t} as BetterAuthOptionsContext<TUses>);\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<AuthServer> {\n\t\t// `Auth<O>` is invariant in its options in better-auth 1.7, so the value\n\t\t// built from a concrete literal is not assignable to the `AuthServer`\n\t\t// alias, which names the constraint. The runtime object is the same one\n\t\t// either way; the assertion keeps the variance where it happens instead\n\t\t// of spreading a generic through the construct's public type.\n\t\treturn betterAuth(await this.optionsFor(options, as)) as AuthServer;\n\t}\n\n\t/**\n\t * Everything better-auth is given: the app's options, and what the construct\n\t * owns — its secret, its URL, its tenant's connection, the origins and cookie\n\t * domain its graph derives. One place, so the server and its migrations are\n\t * built from the same options.\n\t */\n\tprivate async optionsFor(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<Parameters<typeof betterAuth>[0]> {\n\t\tconst { secret, baseUrl, trustedOrigins, cookieDomain, deviceUrl } =\n\t\t\toptions.envParser\n\t\t\t\t.create((get) => ({\n\t\t\t\t\tsecret: get(this.keys.secret).string(),\n\t\t\t\t\t// The surface's own URL, resolved by the target — not guessed from\n\t\t\t\t\t// a port, which was wrong the moment the server moved to 3001.\n\t\t\t\t\tbaseUrl: get(this.keys.url).string(),\n\t\t\t\t\t// Better Auth's CSRF check applies to every caller, not just\n\t\t\t\t\t// browsers, so an API calling the auth server is rejected unless\n\t\t\t\t\t// its origin is trusted. The list is derived from the graph and\n\t\t\t\t\t// arrives as one comma-separated value, for the same reason every\n\t\t\t\t\t// other derived value arrives as a string: it crosses a process\n\t\t\t\t\t// boundary as env.\n\t\t\t\t\ttrustedOrigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.default('')\n\t\t\t\t\t\t.transform((value) =>\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t\t),\n\t\t\t\t\t// The domain a session cookie has to carry to be readable by a\n\t\t\t\t\t// frontend on a sibling host. Optional because there is often\n\t\t\t\t\t// nothing to widen to: locally everything shares `localhost`,\n\t\t\t\t\t// where cookies ignore the port and a `Domain` would only be a\n\t\t\t\t\t// value the browser refuses.\n\t\t\t\t\tcookieDomain: get(this.keys.cookieDomain).string().optional(),\n\t\t\t\t\t// Where a phone reaches this server, on a local stage with a mobile\n\t\t\t\t\t// app: its own port on the LAN address. Never deployed.\n\t\t\t\t\tdeviceUrl: get(provideKey(this.id, 'deviceUrl')).string().optional(),\n\t\t\t\t}))\n\t\t\t\t.parse();\n\n\t\t// The tenant's own client: one construct, one connection, so what auth\n\t\t// writes and what the browser inspects cannot be two different databases.\n\t\t// A migration asks the tenant for its owner connection, which the tenant\n\t\t// exposes as a `Service` and not as anything `.dependsOn()` would take.\n\t\tconst tenant = this.config.database as Consumable & {\n\t\t\towner?: Service<string, unknown>;\n\t\t};\n\t\tconst source = as.owner && tenant.owner ? tenant.owner : tenant.service;\n\n\t\tconst db = (await source.register(options)) as Kysely<\n\t\t\tRecord<string, never>\n\t\t>;\n\n\t\tconst configured = await this.configured(options);\n\n\t\t// Whatever the app added wins over the derived list rather than\n\t\t// replacing it: an origin nobody declared is still sometimes real.\n\t\tconst origins = [\n\t\t\t...trustedOrigins,\n\t\t\t...(configured.trustedOrigins && Array.isArray(configured.trustedOrigins)\n\t\t\t\t? configured.trustedOrigins\n\t\t\t\t: []),\n\t\t];\n\n\t\tconst plugins = [...(configured.plugins ?? [])];\n\t\t// A mobile app depends on this server: its scheme is among the origins.\n\t\t// Better Auth's Expo plugin is what lets it sign in — the app sends its\n\t\t// scheme as the origin, and the plugin is what reads it — so it is added\n\t\t// here rather than written into every app that has one.\n\t\tif (\n\t\t\torigins.some((origin) => !isWebOrigin(origin)) &&\n\t\t\t!plugins.some((plugin) => plugin.id === 'expo')\n\t\t) {\n\t\t\tplugins.push(await expoPlugin(this.id));\n\t\t}\n\t\t// A sign-in link the app asked for is opened on the phone, which cannot\n\t\t// resolve this server's local hostname; on a local stage it is built on\n\t\t// the address the phone reaches it on instead.\n\t\tif (deviceUrl) {\n\t\t\tfor (const plugin of plugins) reachableFromDevice(plugin, deviceUrl);\n\t\t}\n\n\t\treturn {\n\t\t\t...configured,\n\t\t\tsecret,\n\t\t\tbaseURL: baseUrl,\n\t\t\tbasePath: this.basePath,\n\t\t\tdatabase: { db, type: 'postgres' },\n\t\t\tplugins,\n\t\t\ttrustedOrigins: origins,\n\t\t\tadvanced: {\n\t\t\t\t...configured.advanced,\n\t\t\t\t// Only when a domain was derived. Better Auth reads the presence\n\t\t\t\t// of this block as intent, so enabling it with no domain would\n\t\t\t\t// widen the cookie to whatever host happened to set it — and the\n\t\t\t\t// case with nothing to widen to is the common one, not an error.\n\t\t\t\t...(cookieDomain\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tcrossSubDomainCookies: {\n\t\t\t\t\t\t\t\tenabled: true,\n\t\t\t\t\t\t\t\tdomain: cookieDomain,\n\t\t\t\t\t\t\t\t...configured.advanced?.crossSubDomainCookies,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** Better Auth's Expo plugin, which a mobile app's sign-in needs. */\nasync function expoPlugin(\n\tserver: string,\n): Promise<NonNullable<BetterAuthOptions['plugins']>[number]> {\n\ttry {\n\t\tconst { expo } = (await import('@better-auth/expo')) as {\n\t\t\texpo: () => NonNullable<BetterAuthOptions['plugins']>[number];\n\t\t};\n\t\treturn expo();\n\t} catch (error) {\n\t\tthrow new ExpoPluginMissing(server, error);\n\t}\n}\n\n/** Links already rewritten — an `options` object is reused across builds. */\nconst rewritten = new WeakSet<object>();\n\n/**\n * Build the magic links an app asked for on the address a phone reaches.\n *\n * An app's link carries its scheme as the `callbackURL` (`shop-dev://…`)\n * where a browser's carries a path or an `http(s)` URL — which is how the two\n * are told apart. A browser's link is left alone: it is opened on this\n * machine, where the server's own hostname resolves.\n *\n * Better Auth's magic-link plugin calls `sendMagicLink` through the options\n * object it returns, so wrapping it there is wrapping the call.\n */\nfunction reachableFromDevice(plugin: { id: string }, deviceUrl: string): void {\n\tif (plugin.id !== 'magic-link') return;\n\tconst options = (plugin as { options?: MagicLinkOptions }).options;\n\tif (!options?.sendMagicLink || rewritten.has(options)) return;\n\trewritten.add(options);\n\n\tconst send = options.sendMagicLink;\n\toptions.sendMagicLink = (data, ...rest) =>\n\t\tsend({ ...data, url: deviceLink(data.url, deviceUrl) }, ...rest);\n}\n\ninterface MagicLinkOptions {\n\tsendMagicLink?: (\n\t\tdata: { email: string; url: string; token: string },\n\t\t...rest: unknown[]\n\t) => unknown;\n}\n\n/** A magic link moved to the device address when an app is its destination. */\nexport function deviceLink(url: string, deviceUrl: string): string {\n\ttry {\n\t\tconst link = new URL(url);\n\t\tconst callback = link.searchParams.get('callbackURL') ?? '/';\n\t\tif (callback.startsWith('/') || isWebOrigin(callback)) return url;\n\t\treturn new URL(`${link.pathname}${link.search}`, deviceUrl).toString();\n\t} catch {\n\t\treturn url;\n\t}\n}\n\n/** A mobile app depends on this auth server, and `@better-auth/expo` is absent. */\nexport class ExpoPluginMissing extends Error {\n\tconstructor(\n\t\treadonly server: string,\n\t\toverride readonly cause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`'${server}' is called by a mobile app, and a mobile app signs in through ` +\n\t\t\t\t`Better Auth's Expo plugin — install it where the auth server runs: ` +\n\t\t\t\t`pnpm add @better-auth/expo`,\n\t\t);\n\t\tthis.name = 'ExpoPluginMissing';\n\t}\n}\n\n/** The auth server answered a session check with something other than 200. */\nexport class SessionCheckFailed extends Error {\n\tconstructor(\n\t\treadonly authenticator: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`'${authenticator}' answered a session check with ${status}. ` +\n\t\t\t\t`A request with no session gets 200 and null; this is the server ` +\n\t\t\t\t`failing — check that it is running and reachable at its URL.`,\n\t\t);\n\t\tthis.name = 'SessionCheckFailed';\n\t}\n}\n\n/**\n * Better Auth's DDL, safe to run where its tables already exist.\n *\n * Its SQL is Kysely's, so its shape is known: `create table \"…\"`, `create index\n * \"…\"`, `alter table \"…\" add column \"…\"`. A database that has been running\n * already has those — Better Auth used to create them at runtime — and the\n * first committed migration would fail on them rather than recording that they\n * are there.\n */\nexport function idempotent(statements: string): string {\n\treturn statements\n\t\t.replace(/\\bcreate table \"/gi, 'create table if not exists \"')\n\t\t.replace(\n\t\t\t/\\bcreate (unique )?index \"/gi,\n\t\t\t(_, unique = '') => `create ${unique}index if not exists \"`,\n\t\t)\n\t\t.replace(/\\badd column \"/gi, 'add column if not exists \"');\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as StandardSchemaV1 } from "../index-CeEtxDmK.cjs";
|
|
2
2
|
import { t as Construct } from "../Construct-DTqYvkPG.cjs";
|
|
3
|
-
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-
|
|
4
|
-
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-
|
|
3
|
+
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-wV6FcBV0.cjs";
|
|
4
|
+
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-CitKd3JQ.cjs";
|
|
5
5
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
6
6
|
import { EventPublisher, MappedEvent } from "@geekmidas/events";
|
|
7
7
|
import { Logger } from "@geekmidas/logger";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as StandardSchemaV1 } from "../index-CeEtxDmK.mjs";
|
|
2
2
|
import { t as Construct } from "../Construct-pVadmURD.mjs";
|
|
3
|
-
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-
|
|
4
|
-
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-
|
|
3
|
+
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-aG6oyuhR.mjs";
|
|
4
|
+
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-DdMqIfSy.mjs";
|
|
5
5
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
6
6
|
import { EventPublisher, MappedEvent } from "@geekmidas/events";
|
|
7
7
|
import { Service, ServiceDiscovery } from "@geekmidas/services";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_construct_interface = require("./construct-interface.cjs");
|
|
3
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
4
|
+
//#region src/mobile-app.ts
|
|
5
|
+
/**
|
|
6
|
+
* `MobileApp` — an Expo app, declared like a site.
|
|
7
|
+
*
|
|
8
|
+
* Its edges are the point, as a site's are. One `.dependsOn([api, auth])` is
|
|
9
|
+
* the fact behind everything a mobile app otherwise has written down by hand:
|
|
10
|
+
*
|
|
11
|
+
* - its `EXPO_PUBLIC_API_URL` and `EXPO_PUBLIC_AUTH_URL`, which locally are
|
|
12
|
+
* the servers' own ports — reachable once the app swaps `localhost` for the
|
|
13
|
+
* address Metro is served from, which is what makes a phone on the LAN work;
|
|
14
|
+
* - its URL scheme, per stage (`shop`, or `shop-dev` locally), so a
|
|
15
|
+
* development build and the store build on one phone never share links;
|
|
16
|
+
* - the scheme in the auth server's and the API's trusted origins — and, on a
|
|
17
|
+
* local stage, the `exp://` origins Expo Go sends from;
|
|
18
|
+
* - Better Auth's Expo plugin on the auth server, and sign-in links an app asks
|
|
19
|
+
* for built on an address the phone can open.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* export const app = new MobileApp('App', { path: 'apps/app' })
|
|
23
|
+
* .dependsOn([api, auth]);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Like a site it has no `.service`: it is built by its own toolchain, and
|
|
27
|
+
* consumes addresses rather than connections. Nothing deploys it — EAS and the
|
|
28
|
+
* stores do — so the targets derive *for* it and never run it.
|
|
29
|
+
*/
|
|
30
|
+
var MobileApp = class MobileApp {
|
|
31
|
+
config;
|
|
32
|
+
dependencies;
|
|
33
|
+
id;
|
|
34
|
+
/** The key its scheme arrives under — what its `app.config.ts` reads. */
|
|
35
|
+
keys;
|
|
36
|
+
constructor(id, config, dependencies = []) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.dependencies = dependencies;
|
|
39
|
+
this.id = (0, _geekmidas_manifest.canonicalId)(id);
|
|
40
|
+
this.keys = { scheme: (0, _geekmidas_manifest.provideKey)(this.id, "scheme") };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What this app calls — the surfaces it is built with the URLs of, and the
|
|
44
|
+
* ones that come to trust its scheme.
|
|
45
|
+
*
|
|
46
|
+
* Immutable, like every other builder: it returns a new app rather than
|
|
47
|
+
* changing this one.
|
|
48
|
+
*/
|
|
49
|
+
dependsOn(constructs) {
|
|
50
|
+
return new MobileApp(this.id, this.config, [...this.dependencies, ...constructs.map(require_construct_interface.edgeTo)]);
|
|
51
|
+
}
|
|
52
|
+
declare() {
|
|
53
|
+
return [{
|
|
54
|
+
kind: "mobile-app",
|
|
55
|
+
id: this.id,
|
|
56
|
+
variant: this.config.variant ?? "expo",
|
|
57
|
+
app: {
|
|
58
|
+
path: this.config.path,
|
|
59
|
+
...this.config.port ? { port: this.config.port } : {},
|
|
60
|
+
...this.config.config ? { config: this.config.config } : {}
|
|
61
|
+
},
|
|
62
|
+
...this.config.scheme ? { scheme: this.config.scheme } : {},
|
|
63
|
+
dependencies: this.dependencies,
|
|
64
|
+
provides: [this.keys.scheme]
|
|
65
|
+
}];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
exports.MobileApp = MobileApp;
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=mobile-app.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-app.cjs","names":["canonicalId","provideKey","edgeTo"],"sources":["../src/mobile-app.ts"],"sourcesContent":["/**\n * `MobileApp` — an Expo app, declared like a site.\n *\n * Its edges are the point, as a site's are. One `.dependsOn([api, auth])` is\n * the fact behind everything a mobile app otherwise has written down by hand:\n *\n * - its `EXPO_PUBLIC_API_URL` and `EXPO_PUBLIC_AUTH_URL`, which locally are\n * the servers' own ports — reachable once the app swaps `localhost` for the\n * address Metro is served from, which is what makes a phone on the LAN work;\n * - its URL scheme, per stage (`shop`, or `shop-dev` locally), so a\n * development build and the store build on one phone never share links;\n * - the scheme in the auth server's and the API's trusted origins — and, on a\n * local stage, the `exp://` origins Expo Go sends from;\n * - Better Auth's Expo plugin on the auth server, and sign-in links an app asks\n * for built on an address the phone can open.\n *\n * ```ts\n * export const app = new MobileApp('App', { path: 'apps/app' })\n * .dependsOn([api, auth]);\n * ```\n *\n * Like a site it has no `.service`: it is built by its own toolchain, and\n * consumes addresses rather than connections. Nothing deploys it — EAS and the\n * stores do — so the targets derive *for* it and never run it.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\ttype Dependency,\n\ttype MobileAppDeclaration,\n\tprovideKey,\n} from '@geekmidas/manifest';\nimport { type Declarable, edgeTo } from './construct-interface';\n\nexport interface MobileAppConfig {\n\t/**\n\t * Where its source lives, relative to the workspace root — `'apps/app'`.\n\t * Required, the same field a site and a surface take.\n\t */\n\tpath: string;\n\t/**\n\t * The port Metro serves it on locally.\n\t *\n\t * Normally omitted: ports are assigned in a stable order, as a site's are,\n\t * and `gkm exec` hands it to Expo as `RCT_METRO_PORT`.\n\t */\n\tport?: number;\n\t/**\n\t * Modules to import when sniffing which env vars the app reads — its\n\t * values are inlined at build time, as a site's are.\n\t */\n\tconfig?: { client?: string; server?: string };\n\t/**\n\t * Which toolchain builds it — the same field a site names its framework\n\t * by. Expo is the one that ships, and the default.\n\t */\n\tvariant?: MobileAppDeclaration['variant'];\n\t/**\n\t * The scheme's base, before a stage suffix.\n\t *\n\t * Normally omitted: it is the project's name — `shop` — and a local\n\t * stage suffixes it. Give one only when the app is known by another name.\n\t */\n\tscheme?: string;\n}\n\nexport class MobileApp<TName extends string = string>\n\timplements Declarable<TName>\n{\n\treadonly id: TName;\n\t/** The key its scheme arrives under — what its `app.config.ts` reads. */\n\treadonly keys: { scheme: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: MobileAppConfig,\n\t\t/** Internal: how `.dependsOn()` carries edges into the copy it returns. */\n\t\tprivate readonly dependencies: readonly Dependency[] = [],\n\t) {\n\t\tthis.id = canonicalId(id as string) as TName;\n\t\tthis.keys = { scheme: provideKey(this.id, 'scheme') };\n\t}\n\n\t/**\n\t * What this app calls — the surfaces it is built with the URLs of, and the\n\t * ones that come to trust its scheme.\n\t *\n\t * Immutable, like every other builder: it returns a new app rather than\n\t * changing this one.\n\t */\n\tdependsOn(constructs: readonly Declarable[]): MobileApp<TName> {\n\t\treturn new MobileApp<TName>(this.id as ConstructName<TName>, this.config, [\n\t\t\t...this.dependencies,\n\t\t\t...constructs.map(edgeTo),\n\t\t]);\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'mobile-app',\n\t\t\t\tid: this.id,\n\t\t\t\tvariant: this.config.variant ?? 'expo',\n\t\t\t\tapp: {\n\t\t\t\t\tpath: this.config.path,\n\t\t\t\t\t...(this.config.port ? { port: this.config.port } : {}),\n\t\t\t\t\t...(this.config.config ? { config: this.config.config } : {}),\n\t\t\t\t},\n\t\t\t\t...(this.config.scheme ? { scheme: this.config.scheme } : {}),\n\t\t\t\tdependencies: this.dependencies,\n\t\t\t\tprovides: [this.keys.scheme],\n\t\t\t},\n\t\t];\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,YAAb,MAAa,UAEb;CAOmB;CAEA;CARlB;;CAEA;CAEA,YACC,IACA,QAEA,eAAuD,CAAC,GACvD;EAHgB,KAAA,SAAA;EAEA,KAAA,eAAA;EAEjB,KAAK,MAAA,GAAKA,oBAAAA,YAAAA,CAAY,EAAY;EAClC,KAAK,OAAO,EAAE,SAAA,GAAQC,oBAAAA,WAAAA,CAAW,KAAK,IAAI,QAAQ,EAAE;CACrD;;;;;;;;CASA,UAAU,YAAqD;EAC9D,OAAO,IAAI,UAAiB,KAAK,IAA4B,KAAK,QAAQ,CACzE,GAAG,KAAK,cACR,GAAG,WAAW,IAAIC,4BAAAA,MAAM,CACzB,CAAC;CACF;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GACT,SAAS,KAAK,OAAO,WAAW;GAChC,KAAK;IACJ,MAAM,KAAK,OAAO;IAClB,GAAI,KAAK,OAAO,OAAO,EAAE,MAAM,KAAK,OAAO,KAAK,IAAI,CAAC;IACrD,GAAI,KAAK,OAAO,SAAS,EAAE,QAAQ,KAAK,OAAO,OAAO,IAAI,CAAC;GAC5D;GACA,GAAI,KAAK,OAAO,SAAS,EAAE,QAAQ,KAAK,OAAO,OAAO,IAAI,CAAC;GAC3D,cAAc,KAAK;GACnB,UAAU,CAAC,KAAK,KAAK,MAAM;EAC5B,CACD;CACD;AACD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { i as Declarable } from "./construct-interface-DPusCUSQ.cjs";
|
|
2
|
+
import { ConstructName, Declaration, Dependency, MobileAppDeclaration } from "@geekmidas/manifest";
|
|
3
|
+
//#region src/mobile-app.d.ts
|
|
4
|
+
export interface MobileAppConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Where its source lives, relative to the workspace root — `'apps/app'`.
|
|
7
|
+
* Required, the same field a site and a surface take.
|
|
8
|
+
*/
|
|
9
|
+
path: string;
|
|
10
|
+
/**
|
|
11
|
+
* The port Metro serves it on locally.
|
|
12
|
+
*
|
|
13
|
+
* Normally omitted: ports are assigned in a stable order, as a site's are,
|
|
14
|
+
* and `gkm exec` hands it to Expo as `RCT_METRO_PORT`.
|
|
15
|
+
*/
|
|
16
|
+
port?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Modules to import when sniffing which env vars the app reads — its
|
|
19
|
+
* values are inlined at build time, as a site's are.
|
|
20
|
+
*/
|
|
21
|
+
config?: {
|
|
22
|
+
client?: string;
|
|
23
|
+
server?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Which toolchain builds it — the same field a site names its framework
|
|
27
|
+
* by. Expo is the one that ships, and the default.
|
|
28
|
+
*/
|
|
29
|
+
variant?: MobileAppDeclaration['variant'];
|
|
30
|
+
/**
|
|
31
|
+
* The scheme's base, before a stage suffix.
|
|
32
|
+
*
|
|
33
|
+
* Normally omitted: it is the project's name — `shop` — and a local
|
|
34
|
+
* stage suffixes it. Give one only when the app is known by another name.
|
|
35
|
+
*/
|
|
36
|
+
scheme?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class MobileApp<TName extends string = string> implements Declarable<TName> {
|
|
39
|
+
private readonly config;
|
|
40
|
+
/** Internal: how `.dependsOn()` carries edges into the copy it returns. */
|
|
41
|
+
private readonly dependencies;
|
|
42
|
+
readonly id: TName;
|
|
43
|
+
/** The key its scheme arrives under — what its `app.config.ts` reads. */
|
|
44
|
+
readonly keys: {
|
|
45
|
+
scheme: string;
|
|
46
|
+
};
|
|
47
|
+
constructor(id: ConstructName<TName>, config: MobileAppConfig,
|
|
48
|
+
/** Internal: how `.dependsOn()` carries edges into the copy it returns. */
|
|
49
|
+
dependencies?: readonly Dependency[]);
|
|
50
|
+
/**
|
|
51
|
+
* What this app calls — the surfaces it is built with the URLs of, and the
|
|
52
|
+
* ones that come to trust its scheme.
|
|
53
|
+
*
|
|
54
|
+
* Immutable, like every other builder: it returns a new app rather than
|
|
55
|
+
* changing this one.
|
|
56
|
+
*/
|
|
57
|
+
dependsOn(constructs: readonly Declarable[]): MobileApp<TName>;
|
|
58
|
+
declare(): Declaration[];
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//# sourceMappingURL=mobile-app.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-app.d.cts","names":[],"sources":["../src/mobile-app.ts"],"mappings":";;;iBAoCiB;;;;;EAKhB;;;;;;;EAOA;;;;;EAKA;IAAW;IAAiB;;;;;;EAK5B,UAAU;;;;;;;EAOV;;qBAGY,UAAU,0CACX,WAAW;mBAQJ;;mBAEA;WART,IAAI;;WAEJ;IAAQ;;EAEjB,YACC,IAAI,cAAc,QACD,QAAQ,iBAER;;EAAA,wBAAuB;;;;;;;;EAazC,UAAU,qBAAqB,eAAe,UAAU;EAOxD,WAAW"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { i as Declarable } from "./construct-interface-D2gesxos.mjs";
|
|
2
|
+
import { ConstructName, Declaration, Dependency, MobileAppDeclaration } from "@geekmidas/manifest";
|
|
3
|
+
//#region src/mobile-app.d.ts
|
|
4
|
+
export interface MobileAppConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Where its source lives, relative to the workspace root — `'apps/app'`.
|
|
7
|
+
* Required, the same field a site and a surface take.
|
|
8
|
+
*/
|
|
9
|
+
path: string;
|
|
10
|
+
/**
|
|
11
|
+
* The port Metro serves it on locally.
|
|
12
|
+
*
|
|
13
|
+
* Normally omitted: ports are assigned in a stable order, as a site's are,
|
|
14
|
+
* and `gkm exec` hands it to Expo as `RCT_METRO_PORT`.
|
|
15
|
+
*/
|
|
16
|
+
port?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Modules to import when sniffing which env vars the app reads — its
|
|
19
|
+
* values are inlined at build time, as a site's are.
|
|
20
|
+
*/
|
|
21
|
+
config?: {
|
|
22
|
+
client?: string;
|
|
23
|
+
server?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Which toolchain builds it — the same field a site names its framework
|
|
27
|
+
* by. Expo is the one that ships, and the default.
|
|
28
|
+
*/
|
|
29
|
+
variant?: MobileAppDeclaration['variant'];
|
|
30
|
+
/**
|
|
31
|
+
* The scheme's base, before a stage suffix.
|
|
32
|
+
*
|
|
33
|
+
* Normally omitted: it is the project's name — `shop` — and a local
|
|
34
|
+
* stage suffixes it. Give one only when the app is known by another name.
|
|
35
|
+
*/
|
|
36
|
+
scheme?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class MobileApp<TName extends string = string> implements Declarable<TName> {
|
|
39
|
+
private readonly config;
|
|
40
|
+
/** Internal: how `.dependsOn()` carries edges into the copy it returns. */
|
|
41
|
+
private readonly dependencies;
|
|
42
|
+
readonly id: TName;
|
|
43
|
+
/** The key its scheme arrives under — what its `app.config.ts` reads. */
|
|
44
|
+
readonly keys: {
|
|
45
|
+
scheme: string;
|
|
46
|
+
};
|
|
47
|
+
constructor(id: ConstructName<TName>, config: MobileAppConfig,
|
|
48
|
+
/** Internal: how `.dependsOn()` carries edges into the copy it returns. */
|
|
49
|
+
dependencies?: readonly Dependency[]);
|
|
50
|
+
/**
|
|
51
|
+
* What this app calls — the surfaces it is built with the URLs of, and the
|
|
52
|
+
* ones that come to trust its scheme.
|
|
53
|
+
*
|
|
54
|
+
* Immutable, like every other builder: it returns a new app rather than
|
|
55
|
+
* changing this one.
|
|
56
|
+
*/
|
|
57
|
+
dependsOn(constructs: readonly Declarable[]): MobileApp<TName>;
|
|
58
|
+
declare(): Declaration[];
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//# sourceMappingURL=mobile-app.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-app.d.mts","names":[],"sources":["../src/mobile-app.ts"],"mappings":";;;iBAoCiB;;;;;EAKhB;;;;;;;EAOA;;;;;EAKA;IAAW;IAAiB;;;;;;EAK5B,UAAU;;;;;;;EAOV;;qBAGY,UAAU,0CACX,WAAW;mBAQJ;;mBAEA;WART,IAAI;;WAEJ;IAAQ;;EAEjB,YACC,IAAI,cAAc,QACD,QAAQ,iBAER;;EAAA,wBAAuB;;;;;;;;EAazC,UAAU,qBAAqB,eAAe,UAAU;EAOxD,WAAW"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { edgeTo } from "./construct-interface.mjs";
|
|
2
|
+
import { canonicalId, provideKey } from "@geekmidas/manifest";
|
|
3
|
+
//#region src/mobile-app.ts
|
|
4
|
+
/**
|
|
5
|
+
* `MobileApp` — an Expo app, declared like a site.
|
|
6
|
+
*
|
|
7
|
+
* Its edges are the point, as a site's are. One `.dependsOn([api, auth])` is
|
|
8
|
+
* the fact behind everything a mobile app otherwise has written down by hand:
|
|
9
|
+
*
|
|
10
|
+
* - its `EXPO_PUBLIC_API_URL` and `EXPO_PUBLIC_AUTH_URL`, which locally are
|
|
11
|
+
* the servers' own ports — reachable once the app swaps `localhost` for the
|
|
12
|
+
* address Metro is served from, which is what makes a phone on the LAN work;
|
|
13
|
+
* - its URL scheme, per stage (`shop`, or `shop-dev` locally), so a
|
|
14
|
+
* development build and the store build on one phone never share links;
|
|
15
|
+
* - the scheme in the auth server's and the API's trusted origins — and, on a
|
|
16
|
+
* local stage, the `exp://` origins Expo Go sends from;
|
|
17
|
+
* - Better Auth's Expo plugin on the auth server, and sign-in links an app asks
|
|
18
|
+
* for built on an address the phone can open.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* export const app = new MobileApp('App', { path: 'apps/app' })
|
|
22
|
+
* .dependsOn([api, auth]);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Like a site it has no `.service`: it is built by its own toolchain, and
|
|
26
|
+
* consumes addresses rather than connections. Nothing deploys it — EAS and the
|
|
27
|
+
* stores do — so the targets derive *for* it and never run it.
|
|
28
|
+
*/
|
|
29
|
+
var MobileApp = class MobileApp {
|
|
30
|
+
config;
|
|
31
|
+
dependencies;
|
|
32
|
+
id;
|
|
33
|
+
/** The key its scheme arrives under — what its `app.config.ts` reads. */
|
|
34
|
+
keys;
|
|
35
|
+
constructor(id, config, dependencies = []) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.dependencies = dependencies;
|
|
38
|
+
this.id = canonicalId(id);
|
|
39
|
+
this.keys = { scheme: provideKey(this.id, "scheme") };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What this app calls — the surfaces it is built with the URLs of, and the
|
|
43
|
+
* ones that come to trust its scheme.
|
|
44
|
+
*
|
|
45
|
+
* Immutable, like every other builder: it returns a new app rather than
|
|
46
|
+
* changing this one.
|
|
47
|
+
*/
|
|
48
|
+
dependsOn(constructs) {
|
|
49
|
+
return new MobileApp(this.id, this.config, [...this.dependencies, ...constructs.map(edgeTo)]);
|
|
50
|
+
}
|
|
51
|
+
declare() {
|
|
52
|
+
return [{
|
|
53
|
+
kind: "mobile-app",
|
|
54
|
+
id: this.id,
|
|
55
|
+
variant: this.config.variant ?? "expo",
|
|
56
|
+
app: {
|
|
57
|
+
path: this.config.path,
|
|
58
|
+
...this.config.port ? { port: this.config.port } : {},
|
|
59
|
+
...this.config.config ? { config: this.config.config } : {}
|
|
60
|
+
},
|
|
61
|
+
...this.config.scheme ? { scheme: this.config.scheme } : {},
|
|
62
|
+
dependencies: this.dependencies,
|
|
63
|
+
provides: [this.keys.scheme]
|
|
64
|
+
}];
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { MobileApp };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=mobile-app.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-app.mjs","names":[],"sources":["../src/mobile-app.ts"],"sourcesContent":["/**\n * `MobileApp` — an Expo app, declared like a site.\n *\n * Its edges are the point, as a site's are. One `.dependsOn([api, auth])` is\n * the fact behind everything a mobile app otherwise has written down by hand:\n *\n * - its `EXPO_PUBLIC_API_URL` and `EXPO_PUBLIC_AUTH_URL`, which locally are\n * the servers' own ports — reachable once the app swaps `localhost` for the\n * address Metro is served from, which is what makes a phone on the LAN work;\n * - its URL scheme, per stage (`shop`, or `shop-dev` locally), so a\n * development build and the store build on one phone never share links;\n * - the scheme in the auth server's and the API's trusted origins — and, on a\n * local stage, the `exp://` origins Expo Go sends from;\n * - Better Auth's Expo plugin on the auth server, and sign-in links an app asks\n * for built on an address the phone can open.\n *\n * ```ts\n * export const app = new MobileApp('App', { path: 'apps/app' })\n * .dependsOn([api, auth]);\n * ```\n *\n * Like a site it has no `.service`: it is built by its own toolchain, and\n * consumes addresses rather than connections. Nothing deploys it — EAS and the\n * stores do — so the targets derive *for* it and never run it.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\ttype Dependency,\n\ttype MobileAppDeclaration,\n\tprovideKey,\n} from '@geekmidas/manifest';\nimport { type Declarable, edgeTo } from './construct-interface';\n\nexport interface MobileAppConfig {\n\t/**\n\t * Where its source lives, relative to the workspace root — `'apps/app'`.\n\t * Required, the same field a site and a surface take.\n\t */\n\tpath: string;\n\t/**\n\t * The port Metro serves it on locally.\n\t *\n\t * Normally omitted: ports are assigned in a stable order, as a site's are,\n\t * and `gkm exec` hands it to Expo as `RCT_METRO_PORT`.\n\t */\n\tport?: number;\n\t/**\n\t * Modules to import when sniffing which env vars the app reads — its\n\t * values are inlined at build time, as a site's are.\n\t */\n\tconfig?: { client?: string; server?: string };\n\t/**\n\t * Which toolchain builds it — the same field a site names its framework\n\t * by. Expo is the one that ships, and the default.\n\t */\n\tvariant?: MobileAppDeclaration['variant'];\n\t/**\n\t * The scheme's base, before a stage suffix.\n\t *\n\t * Normally omitted: it is the project's name — `shop` — and a local\n\t * stage suffixes it. Give one only when the app is known by another name.\n\t */\n\tscheme?: string;\n}\n\nexport class MobileApp<TName extends string = string>\n\timplements Declarable<TName>\n{\n\treadonly id: TName;\n\t/** The key its scheme arrives under — what its `app.config.ts` reads. */\n\treadonly keys: { scheme: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: MobileAppConfig,\n\t\t/** Internal: how `.dependsOn()` carries edges into the copy it returns. */\n\t\tprivate readonly dependencies: readonly Dependency[] = [],\n\t) {\n\t\tthis.id = canonicalId(id as string) as TName;\n\t\tthis.keys = { scheme: provideKey(this.id, 'scheme') };\n\t}\n\n\t/**\n\t * What this app calls — the surfaces it is built with the URLs of, and the\n\t * ones that come to trust its scheme.\n\t *\n\t * Immutable, like every other builder: it returns a new app rather than\n\t * changing this one.\n\t */\n\tdependsOn(constructs: readonly Declarable[]): MobileApp<TName> {\n\t\treturn new MobileApp<TName>(this.id as ConstructName<TName>, this.config, [\n\t\t\t...this.dependencies,\n\t\t\t...constructs.map(edgeTo),\n\t\t]);\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'mobile-app',\n\t\t\t\tid: this.id,\n\t\t\t\tvariant: this.config.variant ?? 'expo',\n\t\t\t\tapp: {\n\t\t\t\t\tpath: this.config.path,\n\t\t\t\t\t...(this.config.port ? { port: this.config.port } : {}),\n\t\t\t\t\t...(this.config.config ? { config: this.config.config } : {}),\n\t\t\t\t},\n\t\t\t\t...(this.config.scheme ? { scheme: this.config.scheme } : {}),\n\t\t\t\tdependencies: this.dependencies,\n\t\t\t\tprovides: [this.keys.scheme],\n\t\t\t},\n\t\t];\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,YAAb,MAAa,UAEb;CAOmB;CAEA;CARlB;;CAEA;CAEA,YACC,IACA,QAEA,eAAuD,CAAC,GACvD;EAHgB,KAAA,SAAA;EAEA,KAAA,eAAA;EAEjB,KAAK,KAAK,YAAY,EAAY;EAClC,KAAK,OAAO,EAAE,QAAQ,WAAW,KAAK,IAAI,QAAQ,EAAE;CACrD;;;;;;;;CASA,UAAU,YAAqD;EAC9D,OAAO,IAAI,UAAiB,KAAK,IAA4B,KAAK,QAAQ,CACzE,GAAG,KAAK,cACR,GAAG,WAAW,IAAI,MAAM,CACzB,CAAC;CACF;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GACT,SAAS,KAAK,OAAO,WAAW;GAChC,KAAK;IACJ,MAAM,KAAK,OAAO;IAClB,GAAI,KAAK,OAAO,OAAO,EAAE,MAAM,KAAK,OAAO,KAAK,IAAI,CAAC;IACrD,GAAI,KAAK,OAAO,SAAS,EAAE,QAAQ,KAAK,OAAO,OAAO,IAAI,CAAC;GAC5D;GACA,GAAI,KAAK,OAAO,SAAS,EAAE,QAAQ,KAAK,OAAO,OAAO,IAAI,CAAC;GAC3D,cAAc,KAAK;GACnB,UAAU,CAAC,KAAK,KAAK,MAAM;EAC5B,CACD;CACD;AACD"}
|
package/dist/rest-api.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as Declarable, t as Authenticator } from "./construct-interface-DPusCUSQ.cjs";
|
|
2
|
-
import { T as BuiltInSecuritySchemeId } from "./Endpoint-
|
|
3
|
-
import { t as EndpointFactory } from "./EndpointFactory-
|
|
2
|
+
import { T as BuiltInSecuritySchemeId } from "./Endpoint-wV6FcBV0.cjs";
|
|
3
|
+
import { t as EndpointFactory } from "./EndpointFactory-CitKd3JQ.cjs";
|
|
4
4
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
5
|
import { Logger } from "@geekmidas/logger";
|
|
6
6
|
import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
|
package/dist/rest-api.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as Declarable, t as Authenticator } from "./construct-interface-D2gesxos.mjs";
|
|
2
|
-
import { T as BuiltInSecuritySchemeId } from "./Endpoint-
|
|
3
|
-
import { t as EndpointFactory } from "./EndpointFactory-
|
|
2
|
+
import { T as BuiltInSecuritySchemeId } from "./Endpoint-aG6oyuhR.mjs";
|
|
3
|
+
import { t as EndpointFactory } from "./EndpointFactory-DdMqIfSy.mjs";
|
|
4
4
|
import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
|
|
5
5
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
6
6
|
import { Logger } from "@geekmidas/logger";
|
package/dist/site.cjs
CHANGED
|
@@ -21,9 +21,9 @@ let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
|
21
21
|
* asymmetry is the honest one: a site consumes addresses, not connections.
|
|
22
22
|
*
|
|
23
23
|
* Only the static variant ships. SSR deploy shapes are unstable and each one is
|
|
24
|
-
* its own maintenance, so they follow rather than arrive together
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* its own maintenance, so they follow rather than arrive together. A mobile app
|
|
25
|
+
* is not a site — it is reached by a scheme, not a URL — and is its own
|
|
26
|
+
* construct: see `MobileApp`.
|
|
27
27
|
*/
|
|
28
28
|
var StaticSite = class StaticSite {
|
|
29
29
|
config;
|