@tokenoftrust/storefront-runner 2.4.45 → 2.4.47
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/apps/storefront/src/components/blog/ReactionBar.astro +7 -4
- package/apps/storefront/src/components/the-build/TheBuildIndexView.astro +14 -2
- package/apps/storefront/src/layouts/Layout.astro +16 -6
- package/apps/storefront/src/lib/analytics/webVitals.ts +2 -1
- package/apps/storefront/src/lib/basePath.ts +22 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +4 -2
- package/apps/storefront/src/lib/rawMarketingHtml.ts +38 -1
- package/apps/storefront/src/lib/tracking/interactionCollector.ts +4 -1
- package/apps/storefront/src/pages/[...slug].astro +3 -1
- package/apps/storefront/src/pages/index.astro +1 -0
- package/apps/storefront/src/pages/the-build/index.astro +3 -1
- package/apps/storefront/src/pages/the-build/page/[n].astro +3 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/declared-tenant-config.mjs +5 -7
|
@@ -20,7 +20,7 @@ interface Props {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const { collection, slug } = Astro.props;
|
|
23
|
-
const { tenant,
|
|
23
|
+
const { tenant, apiBasePath, cspNonce } = Astro.locals;
|
|
24
24
|
|
|
25
25
|
const META: Record<Reaction, { emoji: string; label: string }> = {
|
|
26
26
|
like: { emoji: "👍", label: "Like" },
|
|
@@ -31,10 +31,13 @@ const META: Record<Reaction, { emoji: string; label: string }> = {
|
|
|
31
31
|
const kv = await readKv("TENANT_CACHE");
|
|
32
32
|
const counts = await getReactionCounts(kv, tenant.tenant_id, collection, slug);
|
|
33
33
|
|
|
34
|
-
// The react endpoint is base-
|
|
35
|
-
// (mirrors the newsletter form action); host-routed tenants get an empty
|
|
34
|
+
// The react endpoint is api-base-scoped like every other same-origin POST
|
|
35
|
+
// (mirrors the newsletter form action); host-routed tenants get an empty
|
|
36
|
+
// prefix. Uses apiBasePath, never basePath — basePath is overridden to a
|
|
37
|
+
// preview/candidate pin's own render prefix, which has no `/api/...` mirror
|
|
38
|
+
// (fb-1791905696516-ubw8a3's class).
|
|
36
39
|
const endpoint = withBase(
|
|
37
|
-
|
|
40
|
+
apiBasePath,
|
|
38
41
|
`/api/blog/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}/react`,
|
|
39
42
|
);
|
|
40
43
|
---
|
|
@@ -13,6 +13,13 @@ import { collectionPathsV1 } from "@/lib/blog/collectionPaths.js";
|
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
basePath: string;
|
|
16
|
+
/**
|
|
17
|
+
* The tenant's REAL api prefix (`Astro.locals.apiBasePath`), used ONLY for
|
|
18
|
+
* the newsletter form's `/api/...` action — never `basePath`, which is
|
|
19
|
+
* overridden to a preview/candidate pin's own render prefix with no
|
|
20
|
+
* `/api/...` mirror (fb-1791905696516-ubw8a3's class).
|
|
21
|
+
*/
|
|
22
|
+
apiBasePath: string;
|
|
16
23
|
canonicalBase: string;
|
|
17
24
|
copy: TheBuildIndexCopy;
|
|
18
25
|
presentation: BlogPresentation;
|
|
@@ -21,7 +28,8 @@ interface Props {
|
|
|
21
28
|
totalPages: number;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
const { basePath, canonicalBase, copy, presentation, pagePosts, page, totalPages } =
|
|
31
|
+
const { basePath, apiBasePath, canonicalBase, copy, presentation, pagePosts, page, totalPages } =
|
|
32
|
+
Astro.props;
|
|
25
33
|
const buildPaths = collectionPathsV1(Astro.locals, "the-build");
|
|
26
34
|
|
|
27
35
|
const href = (path: string) => `${basePath}${path}` || "/";
|
|
@@ -105,7 +113,11 @@ const itemList = {
|
|
|
105
113
|
<section class="news-signup" aria-labelledby="news-signup-title">
|
|
106
114
|
<h2 id="news-signup-title">Get {copy.title} in your inbox</h2>
|
|
107
115
|
<p>A short note when something worth reading lands. Double opt-in — we'll email a link to confirm.</p>
|
|
108
|
-
<form
|
|
116
|
+
<form
|
|
117
|
+
class="news-form"
|
|
118
|
+
method="post"
|
|
119
|
+
action={`${apiBasePath}/api/newsletter/the-build/subscribe`}
|
|
120
|
+
>
|
|
109
121
|
<label class="sr-only" for="news-email">Email address</label>
|
|
110
122
|
<input id="news-email" name="email" type="email" inputmode="email" autocomplete="email"
|
|
111
123
|
required placeholder="you@example.com" />
|
|
@@ -25,7 +25,7 @@ import AuthBadge from "@/components/auth/AuthBadge.astro";
|
|
|
25
25
|
import { referenceTheme } from "@/themes/reference";
|
|
26
26
|
import { mergeTheme } from "@/themes/schema";
|
|
27
27
|
import { organizationLd, websiteLd } from "@/lib/jsonld";
|
|
28
|
-
import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
|
|
28
|
+
import { nonceInlineTags, prefixApiFormActions, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
|
|
29
29
|
import { CHROME_BEHAVIOR_JS, CHROME_HEAD_JS, sanitizeSharedChromeFragment } from "@/lib/rawChrome";
|
|
30
30
|
import { ASYNC_CSS_SWAP_JS } from "@/lib/asyncCss";
|
|
31
31
|
import { reportTrackingChainHealth } from "@/lib/tracking/interactionHealth";
|
|
@@ -106,7 +106,7 @@ const {
|
|
|
106
106
|
headerVariant,
|
|
107
107
|
footerVariant,
|
|
108
108
|
} = Astro.props;
|
|
109
|
-
const { tenant, themeStyle, canonicalBase, basePath, cspNonce, features, capabilities } =
|
|
109
|
+
const { tenant, themeStyle, canonicalBase, basePath, apiBasePath, cspNonce, features, capabilities } =
|
|
110
110
|
Astro.locals;
|
|
111
111
|
|
|
112
112
|
// Fixed-phase integration engine (ext-kernel-04): ONE engine emits the
|
|
@@ -175,12 +175,18 @@ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
|
|
|
175
175
|
const bodyEnd = chromeHtml.lastIndexOf("</body>");
|
|
176
176
|
sharedChrome = {
|
|
177
177
|
header: nonceInlineTags(
|
|
178
|
-
|
|
178
|
+
prefixApiFormActions(
|
|
179
|
+
sanitizeSharedChromeFragment(chromeHtml.slice(bodyOpen, marker)),
|
|
180
|
+
apiBasePath,
|
|
181
|
+
),
|
|
179
182
|
cspNonce,
|
|
180
183
|
),
|
|
181
184
|
footer: nonceInlineTags(
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
prefixApiFormActions(
|
|
186
|
+
sanitizeSharedChromeFragment(
|
|
187
|
+
chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined),
|
|
188
|
+
),
|
|
189
|
+
apiBasePath,
|
|
184
190
|
),
|
|
185
191
|
cspNonce,
|
|
186
192
|
),
|
|
@@ -268,7 +274,11 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
268
274
|
---
|
|
269
275
|
|
|
270
276
|
<!doctype html>
|
|
271
|
-
<html
|
|
277
|
+
<html
|
|
278
|
+
lang={lang ?? tenant.locale.split("-")[0] ?? "en"}
|
|
279
|
+
data-base-path={basePath || undefined}
|
|
280
|
+
data-api-base={apiBasePath || undefined}
|
|
281
|
+
>
|
|
272
282
|
<head>
|
|
273
283
|
<meta charset="utf-8" />
|
|
274
284
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
isPurposeEligible,
|
|
35
35
|
parseConsentSnapshot,
|
|
36
36
|
} from "@tot/public-runtime";
|
|
37
|
+
import { withClientApiBase } from "@/lib/basePath";
|
|
37
38
|
|
|
38
39
|
/** The three field metrics we collect. INP is field-only (no lab equivalent). */
|
|
39
40
|
export type WebVitalName = "lcp" | "inp" | "cls";
|
|
@@ -119,7 +120,7 @@ export const RUM_BEACON_PATH = "/api/rum/vitals";
|
|
|
119
120
|
* `sendBeacon` is fire-and-forget and survives page unload — the moment CWV are
|
|
120
121
|
* finalized. Falls back to a keepalive `fetch` where `sendBeacon` is absent.
|
|
121
122
|
*/
|
|
122
|
-
export function beaconReporter(endpoint: string = RUM_BEACON_PATH): WebVitalsReporter {
|
|
123
|
+
export function beaconReporter(endpoint: string = withClientApiBase(RUM_BEACON_PATH)): WebVitalsReporter {
|
|
123
124
|
return (sample) => {
|
|
124
125
|
if (typeof navigator === "undefined") return;
|
|
125
126
|
const body = JSON.stringify(sample);
|
|
@@ -80,6 +80,28 @@ export function withClientBase(href: string): string {
|
|
|
80
80
|
return withBase(clientBasePath(), href);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The active tenant's REAL api prefix as seen from the browser, read from the
|
|
85
|
+
* `<html data-api-base>` attribute Layout.astro emits (mirrors
|
|
86
|
+
* `locals.apiBasePath`, middleware/renderChain.ts — never a preview/candidate
|
|
87
|
+
* pin's base path). Returns "" on the server, for host-routed tenants, or
|
|
88
|
+
* before hydration.
|
|
89
|
+
*/
|
|
90
|
+
export function clientApiBasePath(): string {
|
|
91
|
+
if (typeof document === "undefined") return "";
|
|
92
|
+
return document.documentElement.dataset.apiBase ?? "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convenience for client beacons that POST to a same-origin `/api/...`
|
|
97
|
+
* endpoint: prefix `path` with the client api base path so the request
|
|
98
|
+
* reaches the tenant's own routed prefix instead of 404ing at the platform
|
|
99
|
+
* host root (fb-1791984159097-jt5w3y's class).
|
|
100
|
+
*/
|
|
101
|
+
export function withClientApiBase(path: string): string {
|
|
102
|
+
return withBase(clientApiBasePath(), path);
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
/**
|
|
84
106
|
* Keep same-origin root-absolute anchors inside the tenant prefix, so navigation on a
|
|
85
107
|
* path-routed page stays on the tenant. Covers anchors in raw tenant HTML and chrome,
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
IntegrationSiteSurface,
|
|
25
25
|
} from "@tot/public-runtime";
|
|
26
26
|
import { INTEGRATION_PLAN_GLOBAL } from "@tot/public-runtime";
|
|
27
|
+
import { withClientApiBase } from "@/lib/basePath";
|
|
27
28
|
|
|
28
29
|
/** Where the browser posts a tamper beacon; the server receiver records + suspends. */
|
|
29
30
|
export const TAMPER_BEACON_ENDPOINT = "/api/compliance/tamper";
|
|
@@ -91,14 +92,15 @@ function beaconSink(win: Window): TamperSink {
|
|
|
91
92
|
const post = (beacon: TamperEvidenceBeacon): void => {
|
|
92
93
|
try {
|
|
93
94
|
const body = JSON.stringify(beacon);
|
|
95
|
+
const endpoint = withClientApiBase(TAMPER_BEACON_ENDPOINT);
|
|
94
96
|
const nav = win.navigator as Navigator & {
|
|
95
97
|
sendBeacon?: (url: string, data?: BodyInit) => boolean;
|
|
96
98
|
};
|
|
97
99
|
if (typeof nav.sendBeacon === "function") {
|
|
98
|
-
nav.sendBeacon(
|
|
100
|
+
nav.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
|
-
void fetch(
|
|
103
|
+
void fetch(endpoint, {
|
|
102
104
|
method: "POST",
|
|
103
105
|
headers: { "content-type": "application/json" },
|
|
104
106
|
body,
|
|
@@ -180,6 +180,31 @@ export function prefixTenantLinks(html: string, basePath: string): string {
|
|
|
180
180
|
return html.replace(ROOT_SERVED_HREF_RE, `href="${basePath}/`);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Matches `action="/api/…` and `formaction="/api/…`, excluding `/api/auth/` —
|
|
185
|
+
* that surface is platform-global and always host-root regardless of tenant
|
|
186
|
+
* routing, mirroring `loginGate.ts`'s `isAuthPath` carve-out (`/api/auth/`).
|
|
187
|
+
*/
|
|
188
|
+
const API_FORM_ACTION_RE = /\b(action|formaction)="\/api\/(?!auth\/)/g;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Prefix root-absolute `/api/…` form targets (`action`/`formaction`) with the
|
|
192
|
+
* tenant's REAL api base path, so a raw/shared-chrome tenant's forms (footer
|
|
193
|
+
* newsletter signup, …) POST to the tenant's own path instead of 404ing at the
|
|
194
|
+
* platform host root under path-prefix routing (fb-1791984159097-jt5w3y).
|
|
195
|
+
*
|
|
196
|
+
* Mirrors {@link prefixTenantLinks}, but keyed on `apiBasePath`, never a
|
|
197
|
+
* preview/candidate pin's `basePath` — a pinned render's own `/api/...` POSTs
|
|
198
|
+
* have no mirror under that pin's prefix and would 404 there too (same class
|
|
199
|
+
* as fb-1791905696516-ubw8a3; see `middleware/renderChain.ts`'s
|
|
200
|
+
* `locals.apiBasePath`). No-op when apiBasePath is empty (host-routed prod,
|
|
201
|
+
* subdomain, custom domain).
|
|
202
|
+
*/
|
|
203
|
+
export function prefixApiFormActions(html: string, apiBasePath: string): string {
|
|
204
|
+
if (!apiBasePath) return html;
|
|
205
|
+
return html.replace(API_FORM_ACTION_RE, (_match, attr: string) => `${attr}="${apiBasePath}/api/`);
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
/** Raw pages with a robots `noindex` directive remain routable but stay out of discovery. */
|
|
184
209
|
export function isIndexableHtml(html: string): boolean {
|
|
185
210
|
for (const match of html.matchAll(/<meta\b([^>]*)>/gi)) {
|
|
@@ -275,6 +300,15 @@ export function rawHtmlResponse(
|
|
|
275
300
|
opts: {
|
|
276
301
|
nonce: string;
|
|
277
302
|
basePath: string;
|
|
303
|
+
/**
|
|
304
|
+
* The tenant's REAL routed api prefix (`locals.apiBasePath`), used ONLY to
|
|
305
|
+
* rewrite `/api/...` form actions ({@link prefixApiFormActions}) — never
|
|
306
|
+
* the same value as `basePath` under a preview/candidate pin, where
|
|
307
|
+
* `basePath` is overridden to the pin's own render prefix. Omitted (the
|
|
308
|
+
* default, "") is a no-op, so every existing caller that hasn't threaded
|
|
309
|
+
* it through yet is unchanged.
|
|
310
|
+
*/
|
|
311
|
+
apiBasePath?: string;
|
|
278
312
|
chromeWrapper?: string | null;
|
|
279
313
|
/**
|
|
280
314
|
* The resolved effective plan for this request — the raw-mount half of the
|
|
@@ -363,7 +397,10 @@ export function rawHtmlResponse(
|
|
|
363
397
|
composed = injectBodyEnd(composed, interactionTrackingScript(opts.nonce));
|
|
364
398
|
composed = injectBodyEnd(composed, asyncCssSwapScript(opts.nonce));
|
|
365
399
|
composed = resolveCanonicalHref(composed, opts.canonicalUrl);
|
|
366
|
-
const doc =
|
|
400
|
+
const doc = prefixApiFormActions(
|
|
401
|
+
prefixTenantLinks(composed, opts.basePath),
|
|
402
|
+
opts.apiBasePath ?? "",
|
|
403
|
+
);
|
|
367
404
|
return new Response(doc, {
|
|
368
405
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
369
406
|
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* violate either.
|
|
27
27
|
*/
|
|
28
28
|
import type { TotInteraction } from "@tot/public-runtime";
|
|
29
|
+
import { withClientApiBase } from "@/lib/basePath";
|
|
29
30
|
|
|
30
31
|
/** Where the browser beacon POSTs interaction batches. */
|
|
31
32
|
export const INTERACTION_BEACON_PATH = "/api/tracking/interaction";
|
|
@@ -81,7 +82,9 @@ export type InteractionBatchSender = (events: InteractionWireEvent[]) => void;
|
|
|
81
82
|
/** Default sender: one `sendBeacon` per flushed batch, falling back to a
|
|
82
83
|
* keepalive `fetch` where `sendBeacon` is absent (mirrors
|
|
83
84
|
* `analytics/webVitals.ts`'s `beaconReporter`). Never throws into the page. */
|
|
84
|
-
export function beaconBatchSender(
|
|
85
|
+
export function beaconBatchSender(
|
|
86
|
+
endpoint: string = withClientApiBase(INTERACTION_BEACON_PATH),
|
|
87
|
+
): InteractionBatchSender {
|
|
85
88
|
return (events) => {
|
|
86
89
|
if (events.length === 0 || typeof navigator === "undefined") return;
|
|
87
90
|
const body = JSON.stringify({ events });
|
|
@@ -16,7 +16,8 @@ import { rawHtmlPolicy, rawHtmlResponse, rawPageCanonicalUrl } from "@/lib/rawMa
|
|
|
16
16
|
import { renderRawComplianceTop, renderRawComplianceBottom } from "@/lib/compliance/rawComplianceNotices";
|
|
17
17
|
import { styleGuideAliasRewrite } from "@/lib/styleGuidePaths";
|
|
18
18
|
|
|
19
|
-
const { tenant, canonicalBase, content, basePath, capabilities, features } =
|
|
19
|
+
const { tenant, canonicalBase, content, basePath, apiBasePath, capabilities, features } =
|
|
20
|
+
Astro.locals;
|
|
20
21
|
const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
21
22
|
|
|
22
23
|
// Defensive: never let the catch-all answer for first-class route namespaces.
|
|
@@ -60,6 +61,7 @@ if (rawHtml && rawPolicy.allowed) {
|
|
|
60
61
|
return rawHtmlResponse(rawHtml, {
|
|
61
62
|
nonce: Astro.locals.cspNonce,
|
|
62
63
|
basePath,
|
|
64
|
+
apiBasePath,
|
|
63
65
|
chromeWrapper,
|
|
64
66
|
plan: Astro.locals.integrationPlan,
|
|
65
67
|
consentSnapshot: Astro.locals.consentSnapshot,
|
|
@@ -10,7 +10,8 @@ import { getTheBuildSettings, listBuildPosts } from "@/lib/the-build/provider.js
|
|
|
10
10
|
import { paginateBlogPosts } from "@/lib/blog/pagination.js";
|
|
11
11
|
import { resolveTheBuildIndexCopy } from "@/lib/the-build/pagination.js";
|
|
12
12
|
|
|
13
|
-
const { tenant, basePath, canonicalBase, content, publicationTimestamp } =
|
|
13
|
+
const { tenant, basePath, apiBasePath, canonicalBase, content, publicationTimestamp } =
|
|
14
|
+
Astro.locals;
|
|
14
15
|
const posts = await listBuildPosts(tenant.tenant_id, { content, now: publicationTimestamp });
|
|
15
16
|
if (posts.length === 0) return new Response(null, { status: 404 });
|
|
16
17
|
|
|
@@ -23,6 +24,7 @@ const { posts: pagePosts, totalPages } = paginateBlogPosts(posts, copy.pageSize,
|
|
|
23
24
|
|
|
24
25
|
<TheBuildIndexView
|
|
25
26
|
basePath={basePath}
|
|
27
|
+
apiBasePath={apiBasePath}
|
|
26
28
|
canonicalBase={canonicalBase}
|
|
27
29
|
copy={copy}
|
|
28
30
|
presentation={resolveBlogPresentation(settings)}
|
|
@@ -12,7 +12,8 @@ import { parseTheBuildPageParam, resolveTheBuildIndexCopy } from "@/lib/the-buil
|
|
|
12
12
|
const page = parseTheBuildPageParam(Astro.params.n);
|
|
13
13
|
if (page === null) return new Response(null, { status: 404 });
|
|
14
14
|
|
|
15
|
-
const { tenant, basePath, canonicalBase, content, publicationTimestamp } =
|
|
15
|
+
const { tenant, basePath, apiBasePath, canonicalBase, content, publicationTimestamp } =
|
|
16
|
+
Astro.locals;
|
|
16
17
|
const posts = await listBuildPosts(tenant.tenant_id, { content, now: publicationTimestamp });
|
|
17
18
|
if (posts.length === 0) return new Response(null, { status: 404 });
|
|
18
19
|
|
|
@@ -27,6 +28,7 @@ if (!sliced) return new Response(null, { status: 404 });
|
|
|
27
28
|
|
|
28
29
|
<TheBuildIndexView
|
|
29
30
|
basePath={basePath}
|
|
31
|
+
apiBasePath={apiBasePath}
|
|
30
32
|
canonicalBase={canonicalBase}
|
|
31
33
|
copy={copy}
|
|
32
34
|
presentation={resolveBlogPresentation(settings)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.47",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -134,18 +134,16 @@ export function applyDeclaredTenantConfig(tenant, declared) {
|
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* The declaration a tenant repo commits at `ref`, read through any source with the
|
|
137
|
-
* reconcile's `readFile(path, ref)` shape. `{}` when the file is absent
|
|
137
|
+
* reconcile's `readFile(path, ref)` shape. `{}` when the file is absent. A read that fails
|
|
138
|
+
* or a file that does not parse THROWS: an unknown declaration is never "declares nothing",
|
|
139
|
+
* which would judge a regulated store as unregulated.
|
|
138
140
|
*
|
|
139
141
|
* @param {{ readFile(path: string, ref: string): Promise<string | null> }} source
|
|
140
142
|
* @param {string} ref
|
|
141
143
|
*/
|
|
142
144
|
export async function readDeclaredTenantConfigAt(source, ref) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return raw ? declaredTenantConfig(JSON.parse(raw)) : {};
|
|
146
|
-
} catch {
|
|
147
|
-
return {};
|
|
148
|
-
}
|
|
145
|
+
const raw = await source.readFile(".tot/config.json", ref);
|
|
146
|
+
return raw ? declaredTenantConfig(JSON.parse(raw)) : {};
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/**
|