@tenderprompt/accounts 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/api.d.ts +27 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +73 -0
- package/dist/api.js.map +1 -0
- package/dist/artifact.d.ts +8 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +205 -0
- package/dist/artifact.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts.d.ts +96 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +5 -0
- package/dist/contracts.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -0
- package/dist/files.d.ts +21 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +263 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +565 -0
- package/dist/main.js.map +1 -0
- package/dist/processes.d.ts +12 -0
- package/dist/processes.d.ts.map +1 -0
- package/dist/processes.js +44 -0
- package/dist/processes.js.map +1 -0
- package/dist/starter.d.ts +20 -0
- package/dist/starter.d.ts.map +1 -0
- package/dist/starter.js +159 -0
- package/dist/starter.js.map +1 -0
- package/package.json +32 -0
- package/skill/tender-accounts/SKILL.md +111 -0
- package/templates/shopify-customer-account/AGENTS.md +14 -0
- package/templates/shopify-customer-account/README.md +44 -0
- package/templates/shopify-customer-account/apps/gateway/AGENTS.md +9 -0
- package/templates/shopify-customer-account/apps/gateway/README.md +9 -0
- package/templates/shopify-customer-account/apps/gateway/database-schema.json +49 -0
- package/templates/shopify-customer-account/apps/gateway/dot.dev.vars.example +11 -0
- package/templates/shopify-customer-account/apps/gateway/migrations/0001_customer_auth.sql +76 -0
- package/templates/shopify-customer-account/apps/gateway/package.json +25 -0
- package/templates/shopify-customer-account/apps/gateway/scripts/build-worker.mjs +15 -0
- package/templates/shopify-customer-account/apps/gateway/scripts/pack-tender-artifact.mjs +84 -0
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/hostname-policy.ts +66 -0
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/schema.ts +90 -0
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/session-store.ts +864 -0
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/shopify-route-adapter.ts +1157 -0
- package/templates/shopify-customer-account/apps/gateway/src/platform/runtime-hostname.ts +46 -0
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.test.ts +18 -0
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.ts +51 -0
- package/templates/shopify-customer-account/apps/gateway/src/runtime-config.test.ts +30 -0
- package/templates/shopify-customer-account/apps/gateway/src/runtime-config.ts +56 -0
- package/templates/shopify-customer-account/apps/gateway/src/worker.ts +85 -0
- package/templates/shopify-customer-account/apps/gateway/tender-accounts.json +10 -0
- package/templates/shopify-customer-account/apps/gateway/tender-app.config.json +40 -0
- package/templates/shopify-customer-account/apps/gateway/tsconfig.json +13 -0
- package/templates/shopify-customer-account/apps/gateway/vitest.config.ts +8 -0
- package/templates/shopify-customer-account/apps/gateway/worker-configuration.d.ts +26 -0
- package/templates/shopify-customer-account/apps/gateway/wrangler.jsonc +42 -0
- package/templates/shopify-customer-account/apps/portal/AGENTS.md +9 -0
- package/templates/shopify-customer-account/apps/portal/README.md +5 -0
- package/templates/shopify-customer-account/apps/portal/index.html +13 -0
- package/templates/shopify-customer-account/apps/portal/package.json +33 -0
- package/templates/shopify-customer-account/apps/portal/scripts/build-worker.mjs +11 -0
- package/templates/shopify-customer-account/apps/portal/scripts/pack-tender-artifact.mjs +63 -0
- package/templates/shopify-customer-account/apps/portal/scripts/portal-worker.mjs +84 -0
- package/templates/shopify-customer-account/apps/portal/scripts/portal-worker.test.mjs +14 -0
- package/templates/shopify-customer-account/apps/portal/src/App.tsx +111 -0
- package/templates/shopify-customer-account/apps/portal/src/main.tsx +10 -0
- package/templates/shopify-customer-account/apps/portal/src/styles.css +54 -0
- package/templates/shopify-customer-account/apps/portal/tender-accounts.json +10 -0
- package/templates/shopify-customer-account/apps/portal/tender-app.config.json +19 -0
- package/templates/shopify-customer-account/apps/portal/tsconfig.json +14 -0
- package/templates/shopify-customer-account/apps/portal/vite.config.ts +7 -0
- package/templates/shopify-customer-account/apps/portal/worker-configuration.d.ts +11 -0
- package/templates/shopify-customer-account/apps/portal/wrangler.jsonc +22 -0
- package/templates/shopify-customer-account/dot.dev.vars.example +3 -0
- package/templates/shopify-customer-account/dot.gitignore +9 -0
- package/templates/shopify-customer-account/package.json +22 -0
- package/templates/shopify-customer-account/scripts/artifact-contract.mjs +109 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
|
|
3
|
+
await build({
|
|
4
|
+
entryPoints: ["src/worker.ts"],
|
|
5
|
+
outfile: ".tender/build/worker.mjs",
|
|
6
|
+
bundle: true,
|
|
7
|
+
format: "esm",
|
|
8
|
+
platform: "neutral",
|
|
9
|
+
target: "es2022",
|
|
10
|
+
external: ["node:*"],
|
|
11
|
+
legalComments: "none",
|
|
12
|
+
sourcemap: false,
|
|
13
|
+
minify: false,
|
|
14
|
+
logLevel: "info",
|
|
15
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parseEnv } from "node:util";
|
|
5
|
+
import {
|
|
6
|
+
bundleDigest,
|
|
7
|
+
exactDatabaseSchema,
|
|
8
|
+
exactPublicVars,
|
|
9
|
+
exactResourceRequests,
|
|
10
|
+
sha256,
|
|
11
|
+
} from "../../../scripts/artifact-contract.mjs";
|
|
12
|
+
|
|
13
|
+
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const config = JSON.parse(await readFile(join(appRoot, "tender-app.config.json"), "utf8"));
|
|
15
|
+
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
|
16
|
+
const sourceRevision = (argumentValue("--source-revision") || process.env.TENDER_SOURCE_REVISION || "").trim().toLowerCase();
|
|
17
|
+
if (!/^[a-f0-9]{40}$/u.test(sourceRevision)) throw new Error("A committed 40-character source revision is required.");
|
|
18
|
+
if (config.schema !== "tender.account-source/v1" || config.runtime?.kind !== "worker"
|
|
19
|
+
|| config.runtime?.compatibilityDate !== "2026-08-25" || config.runtime?.mainModule !== "worker.mjs") {
|
|
20
|
+
throw new Error("The gateway source contract is invalid.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const varsPath = resolve(appRoot, argumentValue("--vars") || ".dev.vars");
|
|
24
|
+
const publicVars = exactPublicVars(
|
|
25
|
+
parseEnv(await readFile(varsPath, "utf8")),
|
|
26
|
+
config.publicVarNames,
|
|
27
|
+
config.optionalPublicVarNames,
|
|
28
|
+
);
|
|
29
|
+
const resources = exactResourceRequests(config.resources);
|
|
30
|
+
const databaseSchema = exactDatabaseSchema(JSON.parse(await readFile(resolve(appRoot, config.databaseSchema), "utf8")));
|
|
31
|
+
if (JSON.stringify(resources) !== JSON.stringify([{ type: "d1", name: "APP_DB" }])
|
|
32
|
+
|| JSON.stringify(config.services) !== JSON.stringify([{ name: "PORTAL_UI", targetProject: "portal-ui" }])) {
|
|
33
|
+
throw new Error("The gateway binding contract is invalid.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const workerBytes = await readFile(join(appRoot, ".tender/build/worker.mjs"));
|
|
37
|
+
const module = {
|
|
38
|
+
path: "worker.mjs",
|
|
39
|
+
sha256: sha256(workerBytes),
|
|
40
|
+
bytes: workerBytes.byteLength,
|
|
41
|
+
contentType: "application/javascript+module",
|
|
42
|
+
};
|
|
43
|
+
const files = [];
|
|
44
|
+
const digest = bundleDigest(module, files, databaseSchema);
|
|
45
|
+
const manifest = {
|
|
46
|
+
schema: "tender.account-artifact/v1",
|
|
47
|
+
app: { slug: config.app.slug, version: packageJson.version, label: config.app.label, fixture: false },
|
|
48
|
+
runtime: {
|
|
49
|
+
kind: "worker",
|
|
50
|
+
compatibilityDate: "2026-08-25",
|
|
51
|
+
mainModule: "worker.mjs",
|
|
52
|
+
assetPrefix: `/_tenant-artifacts/${digest}`,
|
|
53
|
+
},
|
|
54
|
+
module,
|
|
55
|
+
files,
|
|
56
|
+
health: { path: "/api/health" },
|
|
57
|
+
resources,
|
|
58
|
+
services: config.services,
|
|
59
|
+
vars: publicVars,
|
|
60
|
+
databaseSchema,
|
|
61
|
+
};
|
|
62
|
+
const artifactsRoot = join(appRoot, ".tender/artifacts");
|
|
63
|
+
const nextRoot = join(artifactsRoot, "next");
|
|
64
|
+
const outputRoot = join(artifactsRoot, "current");
|
|
65
|
+
await rm(nextRoot, { recursive: true, force: true });
|
|
66
|
+
await mkdir(nextRoot, { recursive: true });
|
|
67
|
+
await writeFile(join(nextRoot, "worker.mjs"), workerBytes);
|
|
68
|
+
await writeFile(join(nextRoot, "tender-accounts-build.json"), `${JSON.stringify({
|
|
69
|
+
schema: "tender.external-app-build/v1",
|
|
70
|
+
sourceRevision,
|
|
71
|
+
bundleDigest: digest,
|
|
72
|
+
manifest,
|
|
73
|
+
}, null, 2)}\n`, "utf8");
|
|
74
|
+
await rm(outputRoot, { recursive: true, force: true });
|
|
75
|
+
await rename(nextRoot, outputRoot);
|
|
76
|
+
process.stdout.write(`${JSON.stringify({ status: "packed", bundleDigest: digest, outputRoot })}\n`);
|
|
77
|
+
|
|
78
|
+
function argumentValue(name) {
|
|
79
|
+
const index = process.argv.indexOf(name);
|
|
80
|
+
if (index === -1) return null;
|
|
81
|
+
const value = process.argv[index + 1];
|
|
82
|
+
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${name}.`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
canonicalApplicationHostname,
|
|
4
|
+
immutablePreviewHostname,
|
|
5
|
+
previewAliasHostname,
|
|
6
|
+
} from "../runtime-hostname";
|
|
7
|
+
import type { CustomerAuthHostnamePolicy } from "./session-store";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Secure default for a Tender-hosted application. It authorizes only the
|
|
11
|
+
* canonical application host plus immutable and alias hosts whose trusted
|
|
12
|
+
* tenant/project digests match this application. Merchants using custom host
|
|
13
|
+
* shapes can replace this policy in their own source.
|
|
14
|
+
*/
|
|
15
|
+
export function createTenderRuntimeHostnamePolicy(input: {
|
|
16
|
+
tenantId: string;
|
|
17
|
+
projectId: string;
|
|
18
|
+
baseDomain: string;
|
|
19
|
+
additionalHostnames?: readonly string[];
|
|
20
|
+
}): CustomerAuthHostnamePolicy {
|
|
21
|
+
const canonicalHostname = canonicalApplicationHostname(input);
|
|
22
|
+
const domain = canonicalHostname.slice(canonicalHostname.indexOf(".") + 1);
|
|
23
|
+
const projectDigest = shortDigest(input.projectId, 10);
|
|
24
|
+
const tenantDigest = shortDigest(input.tenantId, 10);
|
|
25
|
+
const immutablePattern = new RegExp(
|
|
26
|
+
`^r-[a-f0-9]{12}--p-${projectDigest}--t-${tenantDigest}\\.${escaped(domain)}$`,
|
|
27
|
+
);
|
|
28
|
+
const aliasPattern = new RegExp(
|
|
29
|
+
`^b-[a-z0-9][a-z0-9-]{1,11}-[a-f0-9]{8}`
|
|
30
|
+
+ `--p-${projectDigest}--t-${tenantDigest}\\.${escaped(domain)}$`,
|
|
31
|
+
);
|
|
32
|
+
const additionalHostnames = new Set((input.additionalHostnames ?? []).map(normalizedHostname));
|
|
33
|
+
|
|
34
|
+
return ({ projectId, hostname }) => (
|
|
35
|
+
projectId === input.projectId
|
|
36
|
+
&& (
|
|
37
|
+
hostname === canonicalHostname
|
|
38
|
+
|| additionalHostnames.has(hostname)
|
|
39
|
+
|| immutablePattern.test(hostname)
|
|
40
|
+
|| aliasPattern.test(hostname)
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizedHostname(value: string): string {
|
|
46
|
+
const hostname = value.trim().toLowerCase().replace(/\.$/u, "");
|
|
47
|
+
if (!/^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(hostname)) {
|
|
48
|
+
throw new Error("The additional runtime hostname is invalid.");
|
|
49
|
+
}
|
|
50
|
+
return hostname;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compile-time references keep this policy aligned with all three host constructors. */
|
|
54
|
+
export const PUBLIC_CUSTOMER_AUTH_RUNTIME_HOST_CONSTRUCTORS = Object.freeze({
|
|
55
|
+
canonicalApplicationHostname,
|
|
56
|
+
immutablePreviewHostname,
|
|
57
|
+
previewAliasHostname,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function shortDigest(value: string, length: number): string {
|
|
61
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function escaped(value: string): string {
|
|
65
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CustomerAuthD1Database } from "./session-store";
|
|
2
|
+
|
|
3
|
+
export const PUBLIC_CUSTOMER_AUTH_SCHEMA = [
|
|
4
|
+
`CREATE TABLE IF NOT EXISTS public_customer_auth_flows (
|
|
5
|
+
project_id TEXT NOT NULL,
|
|
6
|
+
state_digest TEXT NOT NULL,
|
|
7
|
+
browser_binding_digest TEXT NOT NULL,
|
|
8
|
+
client_key_digest TEXT NOT NULL,
|
|
9
|
+
interaction_mode TEXT NOT NULL,
|
|
10
|
+
pkce_verifier TEXT NOT NULL,
|
|
11
|
+
return_hostname TEXT NOT NULL,
|
|
12
|
+
created_at INTEGER NOT NULL,
|
|
13
|
+
expires_at INTEGER NOT NULL,
|
|
14
|
+
consumed_at INTEGER,
|
|
15
|
+
PRIMARY KEY (project_id, state_digest),
|
|
16
|
+
CHECK (length(project_id) BETWEEN 2 AND 128),
|
|
17
|
+
CHECK (length(state_digest) = 64),
|
|
18
|
+
CHECK (state_digest NOT GLOB '*[^0-9a-f]*'),
|
|
19
|
+
CHECK (length(browser_binding_digest) = 64),
|
|
20
|
+
CHECK (browser_binding_digest NOT GLOB '*[^0-9a-f]*'),
|
|
21
|
+
CHECK (length(client_key_digest) = 64),
|
|
22
|
+
CHECK (client_key_digest NOT GLOB '*[^0-9a-f]*'),
|
|
23
|
+
CHECK (interaction_mode IN ('interactive', 'silent')),
|
|
24
|
+
CHECK (length(pkce_verifier) BETWEEN 43 AND 128),
|
|
25
|
+
CHECK (length(return_hostname) BETWEEN 1 AND 253),
|
|
26
|
+
CHECK (return_hostname = lower(return_hostname)),
|
|
27
|
+
CHECK (expires_at > created_at),
|
|
28
|
+
CHECK (consumed_at IS NULL OR consumed_at BETWEEN created_at AND expires_at)
|
|
29
|
+
)`,
|
|
30
|
+
`CREATE INDEX IF NOT EXISTS public_customer_auth_flows_by_expiry
|
|
31
|
+
ON public_customer_auth_flows (expires_at)`,
|
|
32
|
+
`CREATE TABLE IF NOT EXISTS public_customer_auth_sessions (
|
|
33
|
+
project_id TEXT NOT NULL,
|
|
34
|
+
session_digest TEXT NOT NULL,
|
|
35
|
+
audience_hostname TEXT NOT NULL,
|
|
36
|
+
session_data TEXT NOT NULL,
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
expires_at INTEGER NOT NULL,
|
|
39
|
+
PRIMARY KEY (project_id, session_digest),
|
|
40
|
+
CHECK (length(project_id) BETWEEN 2 AND 128),
|
|
41
|
+
CHECK (length(session_digest) = 64),
|
|
42
|
+
CHECK (session_digest NOT GLOB '*[^0-9a-f]*'),
|
|
43
|
+
CHECK (length(audience_hostname) BETWEEN 1 AND 253),
|
|
44
|
+
CHECK (audience_hostname = lower(audience_hostname)),
|
|
45
|
+
CHECK (length(session_data) BETWEEN 1 AND 65536),
|
|
46
|
+
CHECK (expires_at > created_at)
|
|
47
|
+
)`,
|
|
48
|
+
`CREATE INDEX IF NOT EXISTS public_customer_auth_sessions_by_expiry
|
|
49
|
+
ON public_customer_auth_sessions (expires_at)`,
|
|
50
|
+
`CREATE TABLE IF NOT EXISTS public_customer_auth_handoffs (
|
|
51
|
+
project_id TEXT NOT NULL,
|
|
52
|
+
handoff_digest TEXT NOT NULL,
|
|
53
|
+
return_hostname TEXT NOT NULL,
|
|
54
|
+
session_digest TEXT NOT NULL,
|
|
55
|
+
session_seal TEXT NOT NULL,
|
|
56
|
+
created_at INTEGER NOT NULL,
|
|
57
|
+
expires_at INTEGER NOT NULL,
|
|
58
|
+
claimed_at INTEGER,
|
|
59
|
+
PRIMARY KEY (project_id, handoff_digest),
|
|
60
|
+
CHECK (length(project_id) BETWEEN 2 AND 128),
|
|
61
|
+
CHECK (length(handoff_digest) = 64),
|
|
62
|
+
CHECK (handoff_digest NOT GLOB '*[^0-9a-f]*'),
|
|
63
|
+
CHECK (length(return_hostname) BETWEEN 1 AND 253),
|
|
64
|
+
CHECK (return_hostname = lower(return_hostname)),
|
|
65
|
+
CHECK (length(session_digest) = 64),
|
|
66
|
+
CHECK (session_digest NOT GLOB '*[^0-9a-f]*'),
|
|
67
|
+
CHECK (length(session_seal) BETWEEN 80 AND 100000),
|
|
68
|
+
CHECK (expires_at > created_at),
|
|
69
|
+
CHECK (claimed_at IS NULL OR claimed_at BETWEEN created_at AND expires_at)
|
|
70
|
+
)`,
|
|
71
|
+
`CREATE INDEX IF NOT EXISTS public_customer_auth_handoffs_by_expiry
|
|
72
|
+
ON public_customer_auth_handoffs (expires_at)`,
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prototype bootstrap for the merchant-owned runtime D1. The statements are
|
|
77
|
+
* idempotent, so a template can await one isolate-cached initialization promise
|
|
78
|
+
* before serving auth routes. A future application migration runner can replace
|
|
79
|
+
* this without changing the session-store contract.
|
|
80
|
+
*/
|
|
81
|
+
export async function initializePublicCustomerAuthSchema(
|
|
82
|
+
database: CustomerAuthD1Database,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
for (const statement of PUBLIC_CUSTOMER_AUTH_SCHEMA) {
|
|
85
|
+
const result = await database.prepare(statement).bind().run();
|
|
86
|
+
if (result.success === false) {
|
|
87
|
+
throw new Error("The public customer authentication schema could not be initialized.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|