@sudobility/shapeshyft_service 1.0.4 → 1.0.6
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/CLAUDE.md +11 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/server/api-server.d.ts +43 -0
- package/dist/server/api-server.d.ts.map +1 -0
- package/dist/server/api-server.js +85 -0
- package/dist/server/api-server.js.map +1 -0
- package/dist/server/database.d.ts +22 -0
- package/dist/server/database.d.ts.map +1 -0
- package/dist/server/database.js +35 -0
- package/dist/server/database.js.map +1 -0
- package/dist/server/env.d.ts +31 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +73 -0
- package/dist/server/env.js.map +1 -0
- package/dist/server/firebase-auth.d.ts +20 -0
- package/dist/server/firebase-auth.d.ts.map +1 -0
- package/dist/server/firebase-auth.js +35 -0
- package/dist/server/firebase-auth.js.map +1 -0
- package/dist/server/invitation-email.d.ts +32 -0
- package/dist/server/invitation-email.d.ts.map +1 -0
- package/dist/server/invitation-email.js +80 -0
- package/dist/server/invitation-email.js.map +1 -0
- package/package.json +3 -2
package/CLAUDE.md
CHANGED
|
@@ -31,6 +31,17 @@ private peer (our proxy), preferring `CF-Connecting-IP`, then `X-Forwarded-For`
|
|
|
31
31
|
right to left. Unknown peer means deny. Trusting `CF-Connecting-IP` assumes the
|
|
32
32
|
origin accepts traffic only through Cloudflare and the internal proxy.
|
|
33
33
|
|
|
34
|
+
## Process bootstrap (`src/server/`)
|
|
35
|
+
|
|
36
|
+
What the API shells used to duplicate, as factories they configure:
|
|
37
|
+
`createApiServer` (logger, CORS, 50 MB body limit, `/`, `/health`,
|
|
38
|
+
`/health/ready`, the `/api/v1` mount, and the Bun `fetch` that lifts the idle
|
|
39
|
+
timeout per request for long provider calls), `createLazyDatabase`,
|
|
40
|
+
`createEnvReader` (the shell passes `process.env`), `createFirebaseAuth`, and
|
|
41
|
+
`createInvitationEmailSender` (Resend; entity names are HTML-escaped). A shell
|
|
42
|
+
keeps only its product name, pg schema, key prefixes, and provider key
|
|
43
|
+
management.
|
|
44
|
+
|
|
34
45
|
## Rules
|
|
35
46
|
|
|
36
47
|
- No `process.env`. Add a field to `ShapeshyftServiceConfig` instead.
|
package/dist/index.d.ts
CHANGED
|
@@ -16,4 +16,9 @@ export { DEFAULT_RATE_LIMITS_CONFIG, DEFAULT_ENTITLEMENT_DISPLAY_NAMES, } from "
|
|
|
16
16
|
export * from "./schemas/index.js";
|
|
17
17
|
export { collectForwardingHeaders, FORWARDING_HEADERS, isIpLiteral, isIpv4, isIpv6, isRoutableClientIp, normalizeClientIp, resolveAllowlistIp, resolveCallerIp, } from "./lib/client-ip.js";
|
|
18
18
|
export type { AuthContextVariables } from "./hono-context.js";
|
|
19
|
+
export { createEnvReader, parseEnvFile, type EnvReader } from "./server/env.js";
|
|
20
|
+
export { createLazyDatabase, type LazyDatabase } from "./server/database.js";
|
|
21
|
+
export { createFirebaseAuth, type FirebaseAuthConfig, } from "./server/firebase-auth.js";
|
|
22
|
+
export { createInvitationEmailSender, renderInvitationEmail, escapeHtml, type InvitationEmailConfig, } from "./server/invitation-email.js";
|
|
23
|
+
export { createApiServer, DEFAULT_BODY_LIMIT_BYTES, type ApiServer, type ApiServerConfig, } from "./server/api-server.js";
|
|
19
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,WAAW,EACX,SAAS,EACT,WAAW,EACX,MAAM,EACN,UAAU,EACV,0BAA0B,EAC1B,kBAAkB,EAClB,eAAe,EACf,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,sBAAsB,GAC5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,WAAW,EACX,SAAS,EACT,WAAW,EACX,MAAM,EACN,UAAU,EACV,0BAA0B,EAC1B,kBAAkB,EAClB,eAAe,EACf,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,sBAAsB,GAC5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAG9D,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,UAAU,EACV,KAAK,qBAAqB,GAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,KAAK,SAAS,EACd,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,10 @@ export { getActor, getPermissionErrorStatus, userActor, ENTITY_API_KEY_PERMISSIO
|
|
|
14
14
|
export { DEFAULT_RATE_LIMITS_CONFIG, DEFAULT_ENTITLEMENT_DISPLAY_NAMES, } from "./middleware/rateLimit.js";
|
|
15
15
|
export * from "./schemas/index.js";
|
|
16
16
|
export { collectForwardingHeaders, FORWARDING_HEADERS, isIpLiteral, isIpv4, isIpv6, isRoutableClientIp, normalizeClientIp, resolveAllowlistIp, resolveCallerIp, } from "./lib/client-ip.js";
|
|
17
|
+
// Process bootstrap for the API shells: env, database, auth, email, server.
|
|
18
|
+
export { createEnvReader, parseEnvFile } from "./server/env.js";
|
|
19
|
+
export { createLazyDatabase } from "./server/database.js";
|
|
20
|
+
export { createFirebaseAuth, } from "./server/firebase-auth.js";
|
|
21
|
+
export { createInvitationEmailSender, renderInvitationEmail, escapeHtml, } from "./server/invitation-email.js";
|
|
22
|
+
export { createApiServer, DEFAULT_BODY_LIMIT_BYTES, } from "./server/api-server.js";
|
|
17
23
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,GAGxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,GAGb,MAAM,cAAc,CAAC;AAiBtB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,GAI3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,GAGxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,GAGb,MAAM,cAAc,CAAC;AAiBtB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,GAI3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAG5B,4EAA4E;AAC5E,OAAO,EAAE,eAAe,EAAE,YAAY,EAAkB,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAqB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EACL,kBAAkB,GAEnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,UAAU,GAEX,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,eAAe,EACf,wBAAwB,GAGzB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The API process: Hono app, health checks, and Bun server options
|
|
3
|
+
* @description What every shell's `index.ts` used to build by hand: logging,
|
|
4
|
+
* CORS, a body limit sized for base64 media, liveness and readiness routes,
|
|
5
|
+
* the `/api/v1` mount, database initialization on boot, and a Bun `fetch`
|
|
6
|
+
* that lets a long provider call outlive the server's idle timer.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { sql } from "drizzle-orm";
|
|
10
|
+
import type { Logger } from "../contracts.js";
|
|
11
|
+
/** 50 MB: base64-encoded media inflates payloads by a third. */
|
|
12
|
+
export declare const DEFAULT_BODY_LIMIT_BYTES: number;
|
|
13
|
+
export interface ApiServerConfig {
|
|
14
|
+
/** Shown by `GET /` and in the startup log, e.g. "ShapeShyft API". */
|
|
15
|
+
name: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
/** Everything served under `/api/v1`. */
|
|
18
|
+
routes: Hono;
|
|
19
|
+
/** Used by `/health/ready`; anything with drizzle's `execute`. */
|
|
20
|
+
db: {
|
|
21
|
+
execute(query: ReturnType<typeof sql>): Promise<ArrayLike<unknown>>;
|
|
22
|
+
};
|
|
23
|
+
/** Creates and migrates the schema. Run on `start()`; a failure exits. */
|
|
24
|
+
initDatabase(): Promise<void>;
|
|
25
|
+
port: number;
|
|
26
|
+
bodyLimitBytes?: number;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
}
|
|
29
|
+
export interface ApiServer {
|
|
30
|
+
app: Hono;
|
|
31
|
+
/** Initialize the database, logging, and exit the process on failure. */
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
/** Pass as the module's default export for `bun run`. */
|
|
34
|
+
bunServer: {
|
|
35
|
+
port: number;
|
|
36
|
+
idleTimeout: number;
|
|
37
|
+
fetch(request: Request, server: {
|
|
38
|
+
timeout: (req: Request, seconds: number) => void;
|
|
39
|
+
}): Response | Promise<Response>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export declare function createApiServer(config: ApiServerConfig): ApiServer;
|
|
43
|
+
//# sourceMappingURL=api-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../../src/server/api-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAKlC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,MAAM,EAAE,IAAI,CAAC;IACb,kEAAkE;IAClE,EAAE,EAAE;QAAE,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC;IAC5E,0EAA0E;IAC1E,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,IAAI,CAAC;IACV,yEAAyE;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,yDAAyD;IACzD,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CACH,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE;YAAE,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;SAAE,GAC3D,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;KACjC,CAAC;CACH;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAuFlE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The API process: Hono app, health checks, and Bun server options
|
|
3
|
+
* @description What every shell's `index.ts` used to build by hand: logging,
|
|
4
|
+
* CORS, a body limit sized for base64 media, liveness and readiness routes,
|
|
5
|
+
* the `/api/v1` mount, database initialization on boot, and a Bun `fetch`
|
|
6
|
+
* that lets a long provider call outlive the server's idle timer.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { cors } from "hono/cors";
|
|
10
|
+
import { logger as requestLogger } from "hono/logger";
|
|
11
|
+
import { bodyLimit } from "hono/body-limit";
|
|
12
|
+
import { sql } from "drizzle-orm";
|
|
13
|
+
import { errorResponse, successResponse, } from "@sudobility/shapeshyft_engine/types";
|
|
14
|
+
/** 50 MB: base64-encoded media inflates payloads by a third. */
|
|
15
|
+
export const DEFAULT_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
export function createApiServer(config) {
|
|
17
|
+
const log = config.logger ?? console;
|
|
18
|
+
const maxSize = config.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.use("*", requestLogger());
|
|
21
|
+
app.use("*", cors());
|
|
22
|
+
app.use("*", bodyLimit({
|
|
23
|
+
maxSize,
|
|
24
|
+
onError: c => c.json(errorResponse(`Request body too large. Maximum size is ${Math.round(maxSize / (1024 * 1024))}MB.`), 413),
|
|
25
|
+
}));
|
|
26
|
+
app.get("/", c => c.json(successResponse({
|
|
27
|
+
name: config.name,
|
|
28
|
+
version: config.version ?? "1.0.0",
|
|
29
|
+
status: "healthy",
|
|
30
|
+
})));
|
|
31
|
+
// Liveness: the process answers. Public, no database.
|
|
32
|
+
app.get("/health", c => c.json(successResponse({ status: "healthy" })));
|
|
33
|
+
// Readiness: the database answers. Public; 503 until it does.
|
|
34
|
+
app.get("/health/ready", async (c) => {
|
|
35
|
+
try {
|
|
36
|
+
const result = await config.db.execute(sql `SELECT 1 as ok`);
|
|
37
|
+
if (result.length > 0) {
|
|
38
|
+
return c.json(successResponse({ status: "ready", database: "connected" }));
|
|
39
|
+
}
|
|
40
|
+
return c.json(errorResponse("Database check returned no rows"), 503);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
44
|
+
log.error("Health check failed:", message);
|
|
45
|
+
return c.json(errorResponse(`Database not ready: ${message}`), 503);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
app.route("/api/v1", config.routes);
|
|
49
|
+
return {
|
|
50
|
+
app,
|
|
51
|
+
async start() {
|
|
52
|
+
try {
|
|
53
|
+
await config.initDatabase();
|
|
54
|
+
log.log(`${config.name} running on http://localhost:${config.port}`);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
log.error("Failed to initialize database:", error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
bunServer: {
|
|
62
|
+
port: config.port,
|
|
63
|
+
/*
|
|
64
|
+
Long provider calls must not be cut off by the server's idle timer.
|
|
65
|
+
|
|
66
|
+
Bun closes a connection after `idleTimeout` seconds of silence and caps
|
|
67
|
+
that value at 255 -- and a request here is silent for its whole life,
|
|
68
|
+
because the provider is generating and nothing is written back until it
|
|
69
|
+
finishes. A local model answering with a dense score takes longer than
|
|
70
|
+
that: measured against LM Studio, ordinary parts returned in 86-175s
|
|
71
|
+
while a drum kit, which writes three times the notes per bar, ran past
|
|
72
|
+
four minutes and the socket died under it.
|
|
73
|
+
|
|
74
|
+
`server.timeout(req, 0)` lifts the limit for the request in hand rather
|
|
75
|
+
than for the process, so an ordinary request keeps the protection.
|
|
76
|
+
*/
|
|
77
|
+
idleTimeout: 255,
|
|
78
|
+
fetch(request, server) {
|
|
79
|
+
server.timeout(request, 0);
|
|
80
|
+
return app.fetch(request, server);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=api-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-server.js","sourceRoot":"","sources":["../../src/server/api-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lazy PostgreSQL connection
|
|
3
|
+
* @description Nothing connects at import time: the client is created on first
|
|
4
|
+
* use, so scripts and tests can import a shell's `db` without `DATABASE_URL`.
|
|
5
|
+
*/
|
|
6
|
+
import { type PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
7
|
+
import { type Sql } from "postgres";
|
|
8
|
+
export interface LazyDatabase<TSchema extends Record<string, unknown>> {
|
|
9
|
+
/** A drizzle instance that connects on first property access. */
|
|
10
|
+
db: PostgresJsDatabase<TSchema>;
|
|
11
|
+
/** The underlying postgres.js client, created on first call. */
|
|
12
|
+
client(): Sql;
|
|
13
|
+
/** Close the client, if one was created. The next use reconnects. */
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @param connectionString - Called on first use, so a missing URL fails then,
|
|
18
|
+
* not at import.
|
|
19
|
+
* @param schema - The drizzle schema object.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createLazyDatabase<TSchema extends Record<string, unknown>>(connectionString: () => string, schema: TSchema): LazyDatabase<TSchema>;
|
|
22
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAW,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAiB,EAAE,KAAK,GAAG,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,iEAAiE;IACjE,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAChC,gEAAgE;IAChE,MAAM,IAAI,GAAG,CAAC;IACd,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxE,gBAAgB,EAAE,MAAM,MAAM,EAC9B,MAAM,EAAE,OAAO,GACd,YAAY,CAAC,OAAO,CAAC,CAwBvB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lazy PostgreSQL connection
|
|
3
|
+
* @description Nothing connects at import time: the client is created on first
|
|
4
|
+
* use, so scripts and tests can import a shell's `db` without `DATABASE_URL`.
|
|
5
|
+
*/
|
|
6
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
7
|
+
import postgres from "postgres";
|
|
8
|
+
/**
|
|
9
|
+
* @param connectionString - Called on first use, so a missing URL fails then,
|
|
10
|
+
* not at import.
|
|
11
|
+
* @param schema - The drizzle schema object.
|
|
12
|
+
*/
|
|
13
|
+
export function createLazyDatabase(connectionString, schema) {
|
|
14
|
+
let sql = null;
|
|
15
|
+
let instance = null;
|
|
16
|
+
const client = () => (sql ??= postgres(connectionString()));
|
|
17
|
+
const db = new Proxy({}, {
|
|
18
|
+
get(_, prop) {
|
|
19
|
+
instance ??= drizzle(client(), { schema });
|
|
20
|
+
return instance[prop];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
db,
|
|
25
|
+
client,
|
|
26
|
+
async close() {
|
|
27
|
+
if (sql) {
|
|
28
|
+
await sql.end();
|
|
29
|
+
sql = null;
|
|
30
|
+
instance = null;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAA2B,MAAM,yBAAyB,CAAC;AAC3E,OAAO,QAAsB,MAAM,UAAU,CAAC;AAW9C;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,gBAA8B,EAC9B,MAAe;IAEf,IAAI,GAAG,GAAe,IAAI,CAAC;IAC3B,IAAI,QAAQ,GAAuC,IAAI,CAAC;IAExD,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAE5D,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,EAAiC,EAAE;QACtD,GAAG,CAAC,CAAC,EAAE,IAAI;YACT,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3C,OAAQ,QAAwD,CAAC,IAAI,CAAC,CAAC;QACzE,CAAC;KACF,CAAC,CAAC;IAEH,OAAO;QACL,EAAE;QACF,MAAM;QACN,KAAK,CAAC,KAAK;YACT,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;gBAChB,GAAG,GAAG,IAAI,CAAC;gBACX,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Environment reader with `.env.local` priority
|
|
3
|
+
* @description The API shells read their configuration through this and pass
|
|
4
|
+
* values into the service factories. The shell supplies the environment
|
|
5
|
+
* itself, so this package still never reads `process.env`. `.env.local` is
|
|
6
|
+
* parsed once per reader and wins over that environment, which wins over a
|
|
7
|
+
* default.
|
|
8
|
+
*/
|
|
9
|
+
export interface EnvReader {
|
|
10
|
+
/** The value, or `defaultValue` when unset or empty. */
|
|
11
|
+
get(key: string, defaultValue?: string): string | undefined;
|
|
12
|
+
/** The value; throws when unset or empty. */
|
|
13
|
+
getRequired(key: string): string;
|
|
14
|
+
/** The value as a number, or undefined when unset. */
|
|
15
|
+
getNumber(key: string): number | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Parse `KEY=value` lines, skipping blanks and comments, unquoting values. */
|
|
18
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
19
|
+
/**
|
|
20
|
+
* Create an environment reader.
|
|
21
|
+
*
|
|
22
|
+
* @param options.processEnv - The environment to fall back on; the shell passes
|
|
23
|
+
* `process.env`.
|
|
24
|
+
* @param options.envFile - Path of the override file. Default: `.env.local` in
|
|
25
|
+
* the working directory. A missing file is not an error.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createEnvReader(options: {
|
|
28
|
+
processEnv: Record<string, string | undefined>;
|
|
29
|
+
envFile?: string;
|
|
30
|
+
}): EnvReader;
|
|
31
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5D,6CAA6C;IAC7C,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,sDAAsD;IACtD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYpE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,SAAS,CAyCZ"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Environment reader with `.env.local` priority
|
|
3
|
+
* @description The API shells read their configuration through this and pass
|
|
4
|
+
* values into the service factories. The shell supplies the environment
|
|
5
|
+
* itself, so this package still never reads `process.env`. `.env.local` is
|
|
6
|
+
* parsed once per reader and wins over that environment, which wins over a
|
|
7
|
+
* default.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
/** Parse `KEY=value` lines, skipping blanks and comments, unquoting values. */
|
|
12
|
+
export function parseEnvFile(content) {
|
|
13
|
+
const values = {};
|
|
14
|
+
for (const line of content.split("\n")) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
17
|
+
continue;
|
|
18
|
+
const equals = trimmed.indexOf("=");
|
|
19
|
+
if (equals === -1)
|
|
20
|
+
continue;
|
|
21
|
+
const key = trimmed.slice(0, equals).trim();
|
|
22
|
+
const value = trimmed.slice(equals + 1).trim();
|
|
23
|
+
values[key] = value.replace(/^["']|["']$/g, "");
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an environment reader.
|
|
29
|
+
*
|
|
30
|
+
* @param options.processEnv - The environment to fall back on; the shell passes
|
|
31
|
+
* `process.env`.
|
|
32
|
+
* @param options.envFile - Path of the override file. Default: `.env.local` in
|
|
33
|
+
* the working directory. A missing file is not an error.
|
|
34
|
+
*/
|
|
35
|
+
export function createEnvReader(options) {
|
|
36
|
+
let fileValues = null;
|
|
37
|
+
const fromFile = () => {
|
|
38
|
+
if (fileValues === null) {
|
|
39
|
+
try {
|
|
40
|
+
fileValues = parseEnvFile(readFileSync(options.envFile ?? join(process.cwd(), ".env.local"), "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
fileValues = {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fileValues;
|
|
47
|
+
};
|
|
48
|
+
const processEnv = options.processEnv;
|
|
49
|
+
const get = (key, defaultValue) => {
|
|
50
|
+
const local = fromFile()[key];
|
|
51
|
+
if (local !== undefined && local !== "")
|
|
52
|
+
return local;
|
|
53
|
+
const inherited = processEnv[key];
|
|
54
|
+
if (inherited !== undefined && inherited !== "")
|
|
55
|
+
return inherited;
|
|
56
|
+
return defaultValue;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
get,
|
|
60
|
+
getRequired(key) {
|
|
61
|
+
const value = get(key);
|
|
62
|
+
if (value === undefined || value === "") {
|
|
63
|
+
throw new Error(`Required environment variable ${key} is not set`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
getNumber(key) {
|
|
68
|
+
const value = get(key);
|
|
69
|
+
return value ? Number(value) : undefined;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAClD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,KAAK,CAAC,CAAC;YAAE,SAAS;QAC5B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAG/B;IACC,IAAI,UAAU,GAAkC,IAAI,CAAC;IACrD,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,UAAU,GAAG,YAAY,CACvB,YAAY,CACV,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,EACpD,MAAM,CACP,CACF,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU,GAAG,EAAE,CAAC;YAClB,CAAC;QACH,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;IACF,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAEtC,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,YAAqB,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC;QACtD,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE;YAAE,OAAO,SAAS,CAAC;QAClE,OAAO,YAAY,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO;QACL,GAAG;QACH,WAAW,CAAC,GAAG;YACb,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,aAAa,CAAC,CAAC;YACrE,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,SAAS,CAAC,GAAG;YACX,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Firebase-backed AuthAdapter
|
|
3
|
+
* @description Initializes `@sudobility/auth_service` and wraps its verifier in
|
|
4
|
+
* a 5-minute cache. Disabled (for tests) it initializes nothing and rejects
|
|
5
|
+
* every token, so a suite that forgets to mock auth fails loudly.
|
|
6
|
+
*/
|
|
7
|
+
import type { AuthAdapter } from "../contracts.js";
|
|
8
|
+
export interface FirebaseAuthConfig {
|
|
9
|
+
/** False under test: no Firebase initialization, and tokens are rejected. */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
clientEmail?: string;
|
|
13
|
+
privateKey?: string;
|
|
14
|
+
/** Comma-separated site admin emails. */
|
|
15
|
+
siteAdminEmails?: string;
|
|
16
|
+
/** Verified-token cache lifetime. Default: 5 minutes. */
|
|
17
|
+
cacheTtlMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function createFirebaseAuth(config: FirebaseAuthConfig): AuthAdapter;
|
|
20
|
+
//# sourceMappingURL=firebase-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-auth.d.ts","sourceRoot":"","sources":["../../src/server/firebase-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CA8B1E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Firebase-backed AuthAdapter
|
|
3
|
+
* @description Initializes `@sudobility/auth_service` and wraps its verifier in
|
|
4
|
+
* a 5-minute cache. Disabled (for tests) it initializes nothing and rejects
|
|
5
|
+
* every token, so a suite that forgets to mock auth fails loudly.
|
|
6
|
+
*/
|
|
7
|
+
import { createCachedVerifier, getUserInfo, initializeAuth, isAnonymousUser, isSiteAdmin, } from "@sudobility/auth_service";
|
|
8
|
+
export function createFirebaseAuth(config) {
|
|
9
|
+
if (config.enabled) {
|
|
10
|
+
if (!config.projectId || !config.clientEmail || !config.privateKey) {
|
|
11
|
+
throw new Error("Firebase auth needs projectId, clientEmail and privateKey");
|
|
12
|
+
}
|
|
13
|
+
initializeAuth({
|
|
14
|
+
firebase: {
|
|
15
|
+
projectId: config.projectId,
|
|
16
|
+
clientEmail: config.clientEmail,
|
|
17
|
+
privateKey: config.privateKey,
|
|
18
|
+
},
|
|
19
|
+
siteAdminEmails: config.siteAdminEmails,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const verifier = createCachedVerifier(config.cacheTtlMs ?? 300_000);
|
|
23
|
+
return {
|
|
24
|
+
async verifyIdToken(token) {
|
|
25
|
+
if (!config.enabled) {
|
|
26
|
+
throw new Error("Firebase verification not available in test mode");
|
|
27
|
+
}
|
|
28
|
+
return verifier.verify(token);
|
|
29
|
+
},
|
|
30
|
+
isSiteAdmin,
|
|
31
|
+
isAnonymousUser,
|
|
32
|
+
getUserInfo,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=firebase-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-auth.js","sourceRoot":"","sources":["../../src/server/firebase-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,cAAc,EACd,eAAe,EACf,WAAW,GACZ,MAAM,0BAA0B,CAAC;AAelC,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACnE,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAC;QACJ,CAAC;QACD,cAAc,CAAC;YACb,QAAQ,EAAE;gBACR,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU,EAAE,MAAM,CAAC,UAAU;aAC9B;YACD,eAAe,EAAE,MAAM,CAAC,eAAe;SACxC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,UAAU,IAAI,OAAO,CAAC,CAAC;IAEpE,OAAO;QACL,KAAK,CAAC,aAAa,CAAC,KAAK;YACvB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;YACtE,CAAC;YACD,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QACD,WAAW;QACX,eAAe;QACf,WAAW;KACZ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Invitation email sent through Resend
|
|
3
|
+
*/
|
|
4
|
+
import type { EmailSender, Logger } from "../contracts.js";
|
|
5
|
+
export interface InvitationEmailConfig {
|
|
6
|
+
/** The product named in the email, e.g. "ShapeShyft". */
|
|
7
|
+
productName: string;
|
|
8
|
+
/** Without one, sending is skipped with a warning. */
|
|
9
|
+
resendApiKey?: string;
|
|
10
|
+
senderEmail?: string;
|
|
11
|
+
/** Default: `productName`. */
|
|
12
|
+
senderName?: string;
|
|
13
|
+
/** The web app's origin, for the dashboard link. */
|
|
14
|
+
appUrl?: string;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Escape text for HTML. Entity names are chosen by users and land in an email
|
|
19
|
+
* sent to someone else, so they must not be able to inject markup or links.
|
|
20
|
+
*/
|
|
21
|
+
export declare function escapeHtml(text: string): string;
|
|
22
|
+
/** Subject and HTML body of an invitation, exported for tests. */
|
|
23
|
+
export declare function renderInvitationEmail(params: {
|
|
24
|
+
productName: string;
|
|
25
|
+
entityName: string;
|
|
26
|
+
appUrl: string;
|
|
27
|
+
}): {
|
|
28
|
+
subject: string;
|
|
29
|
+
html: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function createInvitationEmailSender(config: InvitationEmailConfig): EmailSender;
|
|
32
|
+
//# sourceMappingURL=invitation-email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitation-email.d.ts","sourceRoot":"","sources":["../../src/server/invitation-email.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmCpC;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,qBAAqB,GAC5B,WAAW,CA6Bb"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Invitation email sent through Resend
|
|
3
|
+
*/
|
|
4
|
+
import { Resend } from "resend";
|
|
5
|
+
const HTML_ESCAPES = {
|
|
6
|
+
"&": "&",
|
|
7
|
+
"<": "<",
|
|
8
|
+
">": ">",
|
|
9
|
+
'"': """,
|
|
10
|
+
"'": "'",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Escape text for HTML. Entity names are chosen by users and land in an email
|
|
14
|
+
* sent to someone else, so they must not be able to inject markup or links.
|
|
15
|
+
*/
|
|
16
|
+
export function escapeHtml(text) {
|
|
17
|
+
return text.replace(/[&<>"']/g, ch => HTML_ESCAPES[ch]);
|
|
18
|
+
}
|
|
19
|
+
/** Subject and HTML body of an invitation, exported for tests. */
|
|
20
|
+
export function renderInvitationEmail(params) {
|
|
21
|
+
const product = escapeHtml(params.productName);
|
|
22
|
+
const entity = escapeHtml(params.entityName);
|
|
23
|
+
const appUrl = escapeHtml(params.appUrl);
|
|
24
|
+
const dashboardUrl = `${appUrl}/en/dashboard?redirect=/invitations`;
|
|
25
|
+
return {
|
|
26
|
+
// Plain text, since mail clients do not render markup in subjects; line
|
|
27
|
+
// breaks are removed so a name cannot spill into other headers.
|
|
28
|
+
subject: `You've been invited to join ${params.entityName} on ${params.productName}`.replace(/[\r\n]+/g, " "),
|
|
29
|
+
html: `
|
|
30
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px;">
|
|
31
|
+
<h2 style="color: #111; margin-bottom: 16px;">You're invited!</h2>
|
|
32
|
+
<p style="color: #333; font-size: 16px; line-height: 1.5;">
|
|
33
|
+
You've been invited to join <strong>${entity}</strong> on ${product}.
|
|
34
|
+
</p>
|
|
35
|
+
<p style="color: #333; font-size: 16px; line-height: 1.5;">
|
|
36
|
+
Sign in to your ${product} account to accept the invitation:
|
|
37
|
+
</p>
|
|
38
|
+
<a href="${dashboardUrl}"
|
|
39
|
+
style="display: inline-block; background: #111; color: #fff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-size: 16px; margin: 16px 0;">
|
|
40
|
+
Go to Dashboard
|
|
41
|
+
</a>
|
|
42
|
+
<p style="color: #666; font-size: 14px; line-height: 1.5; margin-top: 24px;">
|
|
43
|
+
This invitation will expire in 14 days. If you don't have an account yet, sign up at
|
|
44
|
+
<a href="${appUrl}" style="color: #111;">${appUrl}</a> using this email address
|
|
45
|
+
and the invitation will be automatically accepted.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createInvitationEmailSender(config) {
|
|
52
|
+
const logger = config.logger ?? console;
|
|
53
|
+
const resend = config.resendApiKey ? new Resend(config.resendApiKey) : null;
|
|
54
|
+
if (!resend) {
|
|
55
|
+
logger.warn("RESEND_API_KEY not set — invitation emails will not be sent");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
async sendInvitationEmail({ recipientEmail, entityName }) {
|
|
59
|
+
if (!resend) {
|
|
60
|
+
logger.warn("Skipping invitation email — Resend not configured");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const { subject, html } = renderInvitationEmail({
|
|
64
|
+
productName: config.productName,
|
|
65
|
+
entityName,
|
|
66
|
+
appUrl: config.appUrl ?? "http://localhost:5173",
|
|
67
|
+
});
|
|
68
|
+
const { error } = await resend.emails.send({
|
|
69
|
+
from: `${config.senderName ?? config.productName} <${config.senderEmail ?? "onboarding@resend.dev"}>`,
|
|
70
|
+
to: recipientEmail,
|
|
71
|
+
subject,
|
|
72
|
+
html,
|
|
73
|
+
});
|
|
74
|
+
if (error) {
|
|
75
|
+
throw new Error(`Resend error: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=invitation-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitation-email.js","sourceRoot":"","sources":["../../src/server/invitation-email.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAgBhC,MAAM,YAAY,GAA2B;IAC3C,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,OAAO;CACb,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,CAAE,CAAC,CAAC;AAC3D,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,qBAAqB,CAAC,MAIrC;IACC,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,YAAY,GAAG,GAAG,MAAM,qCAAqC,CAAC;IAEpE,OAAO;QACL,wEAAwE;QACxE,gEAAgE;QAChE,OAAO,EACL,+BAA+B,MAAM,CAAC,UAAU,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CACjF,UAAU,EACV,GAAG,CACJ;QACH,IAAI,EAAE;;;;gDAIsC,MAAM,gBAAgB,OAAO;;;4BAGjD,OAAO;;mBAEhB,YAAY;;;;;;qBAMV,MAAM,0BAA0B,MAAM;;;;KAItD;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,MAA6B;IAE7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,6DAA6D,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO;QACL,KAAK,CAAC,mBAAmB,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE;YACtD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;gBACjE,OAAO;YACT,CAAC;YACD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,qBAAqB,CAAC;gBAC9C,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU;gBACV,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,uBAAuB;aACjD,CAAC,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;gBACzC,IAAI,EAAE,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,WAAW,IAAI,uBAAuB,GAAG;gBACrG,EAAE,EAAE,cAAc;gBAClB,OAAO;gBACP,IAAI;aACL,CAAC,CAAC;YACH,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,iBAAiB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/shapeshyft_service",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Shared tables, routes and middleware for ShapeShyft-style structured-output APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"license": "BUSL-1.1",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@hono/zod-validator": "^0.7.5",
|
|
35
|
-
"@sudobility/shapeshyft_engine": "^1.0.
|
|
35
|
+
"@sudobility/shapeshyft_engine": "^1.0.4",
|
|
36
|
+
"resend": "^6.9.2",
|
|
36
37
|
"zod": "^3.24.0"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|