@teamscala/service-boot 0.23.352 → 0.23.355
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/package.json +1 -1
- package/src/agreement-gate.ts +30 -11
- package/src/discover-blocks.test.ts +20 -0
- package/src/discover-blocks.ts +6 -14
- package/src/read-html-template.test.ts +11 -6
- package/src/read-html-template.ts +25 -6
package/package.json
CHANGED
package/src/agreement-gate.ts
CHANGED
|
@@ -37,10 +37,21 @@ import {
|
|
|
37
37
|
type CertificateData,
|
|
38
38
|
} from "./fn-routers/public-agreement-router.ts";
|
|
39
39
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
40
|
+
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
40
41
|
import * as v from "valibot";
|
|
41
42
|
|
|
42
43
|
const AG_COOKIE = "ag_token";
|
|
43
44
|
|
|
45
|
+
async function findFirstAgreement<T>(input: {
|
|
46
|
+
where: Record<string, unknown>;
|
|
47
|
+
select: Record<string, boolean>;
|
|
48
|
+
}): Promise<T | null> {
|
|
49
|
+
const client = getServerClient() as unknown as {
|
|
50
|
+
agreements: { findFirst: (args: typeof input) => Promise<T | null> };
|
|
51
|
+
};
|
|
52
|
+
return client.agreements.findFirst(input);
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/**
|
|
45
56
|
* Agreement gate display + behaviour, DB-driven (config/content-gate-agreement),
|
|
46
57
|
* never hardcoded — mirrors proposal-gate's loadProposalGateSettings. requireOtp
|
|
@@ -112,9 +123,10 @@ const orgMemberCache = createCache<Map<string, string>>(
|
|
|
112
123
|
* member of `organisationId`. Empty when the org is unknown or has no members.
|
|
113
124
|
*
|
|
114
125
|
* Resolved via a parameterized raw join over better_auth_member + "user" rather
|
|
115
|
-
* than `
|
|
126
|
+
* rather than a service-local Prisma `member` model: `member` is a shared
|
|
127
|
+
* @@map'd Prisma model, and each
|
|
116
128
|
* service's per-service client is generated by live DB introspection from only
|
|
117
|
-
* the tables its service_schema declares — so
|
|
129
|
+
* the tables its service_schema declares — so that model is undefined
|
|
118
130
|
* on a service (scala-sign, scala-proposals) that doesn't list better_auth_member.
|
|
119
131
|
* A read-only raw query needs no model declaration and works on every client,
|
|
120
132
|
* which is what makes org-member VIEW access actually resolve (the typed access
|
|
@@ -128,7 +140,6 @@ async function resolveOrgMemberDirectory(
|
|
|
128
140
|
if (cached !== undefined) return cached;
|
|
129
141
|
const directory = new Map<string, string>();
|
|
130
142
|
try {
|
|
131
|
-
const { getPrisma } = await import("@teamscala/db/prisma-registry");
|
|
132
143
|
const rows = await queryRows`
|
|
133
144
|
SELECT u.email AS email, u.name AS name
|
|
134
145
|
FROM better_auth_member m
|
|
@@ -245,8 +256,13 @@ async function buildSigningSession(
|
|
|
245
256
|
const log = getAppLogger();
|
|
246
257
|
const lower = verifiedEmail.trim().toLowerCase();
|
|
247
258
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
259
|
+
const agreement = await findFirstAgreement<{
|
|
260
|
+
name: string | null;
|
|
261
|
+
signer_name: string | null;
|
|
262
|
+
signed_at: Date | string | null;
|
|
263
|
+
signature_data: unknown;
|
|
264
|
+
organisation_id: string | null;
|
|
265
|
+
}>({
|
|
250
266
|
where: { id: agreementUuid },
|
|
251
267
|
select: {
|
|
252
268
|
name: true,
|
|
@@ -638,8 +654,7 @@ export const agreementGateConfig: ContentGateConfig = {
|
|
|
638
654
|
}
|
|
639
655
|
|
|
640
656
|
try {
|
|
641
|
-
const {
|
|
642
|
-
const agreement = await getPrisma().agreements.findFirst({
|
|
657
|
+
const agreement = await findFirstAgreement<{ id: string; name: string }>({
|
|
643
658
|
where: { page_id: pageId },
|
|
644
659
|
select: { id: true, name: true },
|
|
645
660
|
});
|
|
@@ -814,8 +829,10 @@ async function resolveAgreementSignerEmails(
|
|
|
814
829
|
): Promise<Set<string>> {
|
|
815
830
|
const emails = new Set<string>();
|
|
816
831
|
try {
|
|
817
|
-
const
|
|
818
|
-
|
|
832
|
+
const agreement = await findFirstAgreement<{
|
|
833
|
+
signer_email: string | null;
|
|
834
|
+
signature_data: unknown;
|
|
835
|
+
}>({
|
|
819
836
|
where: { id: agreementId },
|
|
820
837
|
select: { signer_email: true, signature_data: true },
|
|
821
838
|
});
|
|
@@ -849,8 +866,10 @@ async function resolveAgreementAuthorizedEmails(
|
|
|
849
866
|
): Promise<Set<string>> {
|
|
850
867
|
const emails = await resolveAgreementSignerEmails(agreementId);
|
|
851
868
|
try {
|
|
852
|
-
const
|
|
853
|
-
|
|
869
|
+
const agreement = await findFirstAgreement<{
|
|
870
|
+
organisation_id: string | null;
|
|
871
|
+
signature_data: unknown;
|
|
872
|
+
}>({
|
|
854
873
|
where: { id: agreementId },
|
|
855
874
|
select: { organisation_id: true, signature_data: true },
|
|
856
875
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { registerPrismaClient } from "@teamscala/db/prisma-registry";
|
|
3
|
+
import type { PrismaClient } from "@teamscala/db/client";
|
|
4
|
+
import { discoverBlocks } from "./discover-blocks.ts";
|
|
5
|
+
|
|
6
|
+
test("block discovery reads the global registry through the registered database client", async () => {
|
|
7
|
+
let calls = 0;
|
|
8
|
+
registerPrismaClient({
|
|
9
|
+
registry_entries: {
|
|
10
|
+
findMany: async () => {
|
|
11
|
+
calls += 1;
|
|
12
|
+
return [];
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} as unknown as PrismaClient);
|
|
16
|
+
|
|
17
|
+
const result = await discoverBlocks([]);
|
|
18
|
+
expect(calls).toBe(1);
|
|
19
|
+
expect(result).toEqual({ components: {}, loaded: [], skipped: [] });
|
|
20
|
+
});
|
package/src/discover-blocks.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
+
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
25
26
|
import { BlockSchemaConfigSchema } from "@teamscala/db-validation/registry-schemas/block-schema";
|
|
26
|
-
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
27
27
|
import * as v from "valibot";
|
|
28
28
|
import type { RendererComponentEntry } from "./types.ts";
|
|
29
29
|
|
|
@@ -40,18 +40,6 @@ interface BlockSchemaConfig {
|
|
|
40
40
|
defaults?: Record<string, unknown>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
interface RegistryEntriesModel {
|
|
44
|
-
findMany: (args: {
|
|
45
|
-
where: Record<string, unknown>;
|
|
46
|
-
select: Record<string, unknown>;
|
|
47
|
-
orderBy: Record<string, unknown>;
|
|
48
|
-
}) => Promise<Array<{ slug: string; config: unknown }>>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface PrismaWithRegistry {
|
|
52
|
-
registry_entries: RegistryEntriesModel;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
43
|
export async function discoverBlocks(
|
|
56
44
|
blockPackages: string[],
|
|
57
45
|
): Promise<DiscoveredBlocks> {
|
|
@@ -92,7 +80,11 @@ export async function discoverBlocks(
|
|
|
92
80
|
const loaded: string[] = [];
|
|
93
81
|
const skipped: string[] = [];
|
|
94
82
|
|
|
95
|
-
|
|
83
|
+
// Block schemas are global registry data, not a model owned by this UI
|
|
84
|
+
// service. The page-server ORPC client is intentionally service-scoped and
|
|
85
|
+
// therefore cannot expose registry_entries. Use the registered database
|
|
86
|
+
// client for this shared model, as every other registry reader does.
|
|
87
|
+
const prisma = getPrisma();
|
|
96
88
|
const rows: Array<{ slug: string; config: unknown }> =
|
|
97
89
|
await prisma.registry_entries.findMany({
|
|
98
90
|
where: { type: "block_schema", is_active: true },
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* @status handwritten
|
|
4
4
|
* @edit edit directly
|
|
5
5
|
*
|
|
6
|
-
* Regression guard for the
|
|
6
|
+
* Regression guard for the runtime-owned Puck document and manifest-keyed
|
|
7
|
+
* HTML cache. The lifetime-pinned
|
|
7
8
|
* cache served a deleted bundle hash after a post-boot rebuild (scala-docs
|
|
8
9
|
* 2026-06-13): build-client-assets rewrites asset-manifest.json + deletes the
|
|
9
10
|
* old hashed bundle, but the cached HTML kept the old hash → 404 → no
|
|
@@ -29,12 +30,7 @@ function writeManifest(hydrate: string, styles: string): void {
|
|
|
29
30
|
beforeEach(() => {
|
|
30
31
|
origCwd = process.cwd();
|
|
31
32
|
dir = mkdtempSync(join(tmpdir(), "read-html-template-"));
|
|
32
|
-
mkdirSync(join(dir, "src"), { recursive: true });
|
|
33
33
|
mkdirSync(join(dir, "dist", "client"), { recursive: true });
|
|
34
|
-
writeFileSync(
|
|
35
|
-
join(dir, "src", "index.html"),
|
|
36
|
-
'<link href="{{stylesPath}}"><script src="{{hydratePath}}"></script>',
|
|
37
|
-
);
|
|
38
34
|
process.chdir(dir);
|
|
39
35
|
});
|
|
40
36
|
|
|
@@ -44,6 +40,15 @@ afterEach(() => {
|
|
|
44
40
|
});
|
|
45
41
|
|
|
46
42
|
describe("readHtmlTemplate manifest-keyed cache", () => {
|
|
43
|
+
test("renders the shared Puck document without a per-service HTML file", async () => {
|
|
44
|
+
writeManifest("/assets/hydrate-aaa.js", "/assets/styles-aaa.css");
|
|
45
|
+
const html = await readHtmlTemplate();
|
|
46
|
+
expect(html).toContain('<div id="__page"><!--ssr-outlet--></div>');
|
|
47
|
+
expect(html).toContain("<!--head-outlet-->");
|
|
48
|
+
expect(html).toContain("<!--boot-data-->");
|
|
49
|
+
expect(await Bun.file(join(dir, "src", "index.html")).exists()).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
47
52
|
test("substitutes the current manifest hashes", async () => {
|
|
48
53
|
writeManifest("/assets/hydrate-aaa.js", "/assets/styles-aaa.css");
|
|
49
54
|
const html = await readHtmlTemplate();
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* @status handwritten
|
|
4
4
|
* @edit edit directly
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* `@teamscala/scala-install/src/runtime/index.html`) and substitutes the
|
|
8
|
-
* `{{stylesPath}}` / `{{hydratePath}}` placeholders with the current
|
|
6
|
+
* Builds the shared React/Puck document envelope and substitutes the current
|
|
9
7
|
* content-hashed asset URLs from `dist/client/asset-manifest.json`.
|
|
10
8
|
*
|
|
9
|
+
* The envelope is runtime-owned. UI services are Puck compositions and must
|
|
10
|
+
* not carry or generate a per-service `src/index.html`; requiring that file
|
|
11
|
+
* creates a second rendering surface that can disappear independently of the
|
|
12
|
+
* Hono + React/Puck runtime.
|
|
13
|
+
*
|
|
11
14
|
* Build-time produces hashed filenames (e.g. `styles-abc123.css`); the
|
|
12
15
|
* static-asset handler serves them with `cache-control: immutable`. New
|
|
13
16
|
* deployments emit new hashes → new URLs → browsers refetch without ever
|
|
@@ -49,6 +52,24 @@ const FALLBACK: AssetManifest = {
|
|
|
49
52
|
hydratePath: "/assets/hydrate.js",
|
|
50
53
|
};
|
|
51
54
|
|
|
55
|
+
const PAGE_DOCUMENT = `<!doctype html>
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="utf-8" />
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
60
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
61
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
62
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=optional" />
|
|
63
|
+
<link rel="stylesheet" href="{{stylesPath}}" />
|
|
64
|
+
<!--head-outlet-->
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<!--boot-data-->
|
|
68
|
+
<div id="__page"><!--ssr-outlet--></div>
|
|
69
|
+
<script type="module" src="{{hydratePath}}"></script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>`;
|
|
72
|
+
|
|
52
73
|
let cached: { html: string; manifestMtime: number } | null = null;
|
|
53
74
|
|
|
54
75
|
function manifestPath(): string {
|
|
@@ -79,10 +100,8 @@ async function loadManifest(): Promise<AssetManifest> {
|
|
|
79
100
|
export async function readHtmlTemplate(): Promise<string> {
|
|
80
101
|
const mtime = await manifestMtimeMs();
|
|
81
102
|
if (cached && cached.manifestMtime === mtime) return cached.html;
|
|
82
|
-
const templatePath = join(process.cwd(), "src", "index.html");
|
|
83
|
-
const raw = await Bun.file(templatePath).text();
|
|
84
103
|
const manifest = await loadManifest();
|
|
85
|
-
let html =
|
|
104
|
+
let html = PAGE_DOCUMENT
|
|
86
105
|
.replace(/\{\{stylesPath\}\}/g, manifest.stylesPath)
|
|
87
106
|
.replace(/\{\{hydratePath\}\}/g, manifest.hydratePath);
|
|
88
107
|
// Tombstone stamp — surfaces a failed client build at the served layer.
|