@volter/twin-googlefonts 0.0.0-stage → 1.0.1

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.
Files changed (173) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +15 -2
  3. package/api/spec/SOURCE.md +17 -0
  4. package/api/spec/discovery.json +306 -0
  5. package/api/spec/openapi.json +647 -0
  6. package/api/spec/sources.json +6 -0
  7. package/api/src/fetch.ts +16 -0
  8. package/api/src/generated/surface.gen.json +1 -0
  9. package/api/src/generated/ui.gen.json +1 -0
  10. package/api/src/index.ts +3 -0
  11. package/api/src/manifest.ts +31 -0
  12. package/api/src/semantics/doors.ts +131 -0
  13. package/api/src/semantics/index.ts +8 -0
  14. package/api/src/semantics/shared.ts +1 -0
  15. package/api/src/semantics/states.ts +6 -0
  16. package/api/src/semantics/webfonts.ts +32 -0
  17. package/assets/spec/SOURCE.md +11 -0
  18. package/assets/spec/openapi.json +162 -0
  19. package/assets/spec/sources.json +1 -0
  20. package/assets/src/fetch.ts +14 -0
  21. package/assets/src/generated/surface.gen.json +1 -0
  22. package/assets/src/generated/ui.gen.json +1 -0
  23. package/assets/src/index.ts +3 -0
  24. package/assets/src/manifest.ts +19 -0
  25. package/assets/src/semantics/index.ts +10 -0
  26. package/assets/src/semantics/l.ts +4 -0
  27. package/assets/src/semantics/s.ts +4 -0
  28. package/assets/src/semantics/shared.ts +13 -0
  29. package/css/spec/SOURCE.md +15 -0
  30. package/css/spec/openapi.json +142 -0
  31. package/css/spec/sources.json +1 -0
  32. package/css/src/fetch.ts +14 -0
  33. package/css/src/generated/surface.gen.json +1 -0
  34. package/css/src/generated/ui.gen.json +1 -0
  35. package/css/src/index.ts +3 -0
  36. package/css/src/manifest.ts +19 -0
  37. package/css/src/semantics/css.ts +19 -0
  38. package/css/src/semantics/css2.ts +20 -0
  39. package/css/src/semantics/index.ts +10 -0
  40. package/css/src/semantics/shared.ts +1 -0
  41. package/dist/api/spec/SOURCE.md +17 -0
  42. package/dist/api/spec/discovery.json +306 -0
  43. package/dist/api/spec/openapi.json +647 -0
  44. package/dist/api/spec/sources.json +6 -0
  45. package/dist/api/src/fetch.d.ts +2 -0
  46. package/dist/api/src/fetch.js +15 -0
  47. package/dist/api/src/fetch.ts +16 -0
  48. package/dist/api/src/generated/surface.gen.json +1 -0
  49. package/dist/api/src/generated/ui.gen.json +1 -0
  50. package/dist/api/src/index.d.ts +2 -0
  51. package/dist/api/src/index.js +3 -0
  52. package/dist/api/src/index.ts +3 -0
  53. package/dist/api/src/manifest.d.ts +2 -0
  54. package/dist/api/src/manifest.js +29 -0
  55. package/dist/api/src/manifest.ts +31 -0
  56. package/dist/api/src/semantics/doors.d.ts +3 -0
  57. package/dist/api/src/semantics/doors.js +138 -0
  58. package/dist/api/src/semantics/doors.ts +131 -0
  59. package/dist/api/src/semantics/index.d.ts +2 -0
  60. package/dist/api/src/semantics/index.js +4 -0
  61. package/dist/api/src/semantics/index.ts +8 -0
  62. package/dist/api/src/semantics/shared.d.ts +1 -0
  63. package/dist/api/src/semantics/shared.js +1 -0
  64. package/dist/api/src/semantics/shared.ts +1 -0
  65. package/dist/api/src/semantics/states.d.ts +8 -0
  66. package/dist/api/src/semantics/states.js +6 -0
  67. package/dist/api/src/semantics/states.ts +6 -0
  68. package/dist/api/src/semantics/webfonts.d.ts +2 -0
  69. package/dist/api/src/semantics/webfonts.js +34 -0
  70. package/dist/api/src/semantics/webfonts.ts +32 -0
  71. package/dist/assets/spec/SOURCE.md +11 -0
  72. package/dist/assets/spec/openapi.json +162 -0
  73. package/dist/assets/spec/sources.json +1 -0
  74. package/dist/assets/src/fetch.d.ts +2 -0
  75. package/dist/assets/src/fetch.js +13 -0
  76. package/dist/assets/src/fetch.ts +14 -0
  77. package/dist/assets/src/generated/surface.gen.json +1 -0
  78. package/dist/assets/src/generated/ui.gen.json +1 -0
  79. package/dist/assets/src/index.d.ts +2 -0
  80. package/dist/assets/src/index.js +3 -0
  81. package/dist/assets/src/index.ts +3 -0
  82. package/dist/assets/src/manifest.d.ts +2 -0
  83. package/dist/assets/src/manifest.js +17 -0
  84. package/dist/assets/src/manifest.ts +19 -0
  85. package/dist/assets/src/semantics/index.d.ts +2 -0
  86. package/dist/assets/src/semantics/index.js +6 -0
  87. package/dist/assets/src/semantics/index.ts +10 -0
  88. package/dist/assets/src/semantics/l.d.ts +2 -0
  89. package/dist/assets/src/semantics/l.js +3 -0
  90. package/dist/assets/src/semantics/l.ts +4 -0
  91. package/dist/assets/src/semantics/s.d.ts +2 -0
  92. package/dist/assets/src/semantics/s.js +3 -0
  93. package/dist/assets/src/semantics/s.ts +4 -0
  94. package/dist/assets/src/semantics/shared.d.ts +2 -0
  95. package/dist/assets/src/semantics/shared.js +13 -0
  96. package/dist/assets/src/semantics/shared.ts +13 -0
  97. package/dist/css/spec/SOURCE.md +15 -0
  98. package/dist/css/spec/openapi.json +142 -0
  99. package/dist/css/spec/sources.json +1 -0
  100. package/dist/css/src/fetch.d.ts +2 -0
  101. package/dist/css/src/fetch.js +13 -0
  102. package/dist/css/src/fetch.ts +14 -0
  103. package/dist/css/src/generated/surface.gen.json +1 -0
  104. package/dist/css/src/generated/ui.gen.json +1 -0
  105. package/dist/css/src/index.d.ts +2 -0
  106. package/dist/css/src/index.js +3 -0
  107. package/dist/css/src/index.ts +3 -0
  108. package/dist/css/src/manifest.d.ts +2 -0
  109. package/dist/css/src/manifest.js +16 -0
  110. package/dist/css/src/manifest.ts +19 -0
  111. package/dist/css/src/semantics/css.d.ts +2 -0
  112. package/dist/css/src/semantics/css.js +19 -0
  113. package/dist/css/src/semantics/css.ts +19 -0
  114. package/dist/css/src/semantics/css2.d.ts +2 -0
  115. package/dist/css/src/semantics/css2.js +21 -0
  116. package/dist/css/src/semantics/css2.ts +20 -0
  117. package/dist/css/src/semantics/index.d.ts +2 -0
  118. package/dist/css/src/semantics/index.js +6 -0
  119. package/dist/css/src/semantics/index.ts +10 -0
  120. package/dist/css/src/semantics/shared.d.ts +1 -0
  121. package/dist/css/src/semantics/shared.js +1 -0
  122. package/dist/css/src/semantics/shared.ts +1 -0
  123. package/dist/journeys/customer-life.json +463 -0
  124. package/dist/journeys/customer-life.outline.md +24 -0
  125. package/dist/journeys/customer-life.outline.verdict.json +82 -0
  126. package/dist/journeys/customer-life.verdict.json +149 -0
  127. package/dist/journeys/decisions.json +54 -0
  128. package/dist/journeys/demand.json +67 -0
  129. package/dist/journeys/first-use.json +20 -0
  130. package/dist/src/cli.d.ts +2 -0
  131. package/dist/src/cli.js +21 -0
  132. package/dist/src/fetch.d.ts +2 -0
  133. package/dist/src/fetch.js +10 -0
  134. package/dist/src/index.d.ts +5 -0
  135. package/dist/src/index.js +15 -0
  136. package/dist/src/manifest.d.ts +2 -0
  137. package/dist/src/manifest.js +25 -0
  138. package/dist/src/semantics/seed.d.ts +2 -0
  139. package/dist/src/semantics/seed.js +141 -0
  140. package/dist/src/semantics/shared.d.ts +9 -0
  141. package/dist/src/semantics/shared.js +23 -0
  142. package/dist/src/server.d.ts +7 -0
  143. package/dist/src/server.js +7 -0
  144. package/dist/vendor/Inter-OFL.txt +93 -0
  145. package/dist/vendor/OFL.txt +94 -0
  146. package/dist/vendor/SOURCE.md +33 -0
  147. package/dist/vendor/antic.json +93 -0
  148. package/dist/vendor/inputs.json +460 -0
  149. package/dist/vendor/inter.json +44 -0
  150. package/dist/vendor/noto-sans-sc-text.json +24 -0
  151. package/generated/pack-facts.json +65 -0
  152. package/journeys/customer-life.json +463 -0
  153. package/journeys/customer-life.outline.md +24 -0
  154. package/journeys/customer-life.outline.verdict.json +82 -0
  155. package/journeys/customer-life.verdict.json +149 -0
  156. package/journeys/decisions.json +54 -0
  157. package/journeys/demand.json +67 -0
  158. package/journeys/first-use.json +20 -0
  159. package/package.json +41 -4
  160. package/src/cli.ts +22 -0
  161. package/src/fetch.ts +11 -0
  162. package/src/index.ts +17 -0
  163. package/src/manifest.ts +27 -0
  164. package/src/semantics/seed.ts +143 -0
  165. package/src/semantics/shared.ts +25 -0
  166. package/src/server.ts +8 -0
  167. package/vendor/Inter-OFL.txt +93 -0
  168. package/vendor/OFL.txt +94 -0
  169. package/vendor/SOURCE.md +33 -0
  170. package/vendor/antic.json +93 -0
  171. package/vendor/inputs.json +460 -0
  172. package/vendor/inter.json +44 -0
  173. package/vendor/noto-sans-sc-text.json +24 -0
@@ -0,0 +1,3 @@
1
+ // The api lane of the googlefonts pack: the vendor's manifest routes its requests here.
2
+ export { createApiLaneFetch } from './fetch.ts';
3
+ export { manifest } from './manifest.ts';
@@ -0,0 +1,31 @@
1
+ // Vendor wire facts; the surface is derived from the retained spec.
2
+ import type { DerivedManifest } from '@volter/world-core';
3
+ import { states } from './semantics/states.ts';
4
+ export const manifest: DerivedManifest = {
5
+ vendor: 'googlefonts', service: 'googlefonts', body: { json: 'always' },
6
+ ids: { template: '{uuid}' }, time: 'iso',
7
+ error: { error: { code: '{status}', message: '{message}', status: '{kind}' } },
8
+ readOnly: { status: 403, kind: 'PERMISSION_DENIED', message: 'Read-only World' },
9
+ notFound: { status: 404, kind: 'NOT_FOUND', message: 'Not Found' },
10
+ gap: { status: 404, kind: 'NOT_FOUND', message: 'Not Found' },
11
+ list: { style: 'envelope', envelope: { items: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
12
+ deleted: {},
13
+ resources: { webfont: { idPrefix: '', ...states.webfont,
14
+ refresh: { none: 'The selected reference snapshot is publisher data, not an account-owned font mutation; no full-live-catalog refresh is claimed.' } },
15
+ _project: { idPrefix: '' },
16
+ _api_key: { idPrefix: '', ...states._api_key } },
17
+ discovery: { twinOf: 'Google Fonts Developer API', stores: 'Published catalog and font representations in the shared googlefonts store' },
18
+ // The pinned helper uses the older documented prefix; no app request is rewritten.
19
+ pathPrefix: '^/webfonts(?=/v1/webfonts$)',
20
+ // The core reads query keys and gates against actual issued hashes. Doors bypass the API gate.
21
+ auth: { header: 'x-goog-api-key', scheme: '', also: [{ query: 'key' }], paths: '^/(webfonts/)?v1/webfonts$',
22
+ missing: { status: 403, kind: 'PERMISSION_DENIED', message: 'The request is missing a valid API key.' },
23
+ invalid: { status: 400, kind: 'INVALID_ARGUMENT', message: 'API key not valid. Please pass a valid API key.' },
24
+ invalidKeys: [], gateWhenAbsent: true, held: { storedAs: '_api_key', hashField: 'sha256' } },
25
+ doors: [{ id: 'appCredentials', method: 'POST', path: '/_twin/app-credentials', note: 'Issue the World application key' },
26
+ { id: 'publishCatalog', method: 'POST', path: '/_twin/catalog', note: 'Google publishes fonts without a Developer API create operation; import its actual catalog/CSS/blob snapshot' }],
27
+ descriptor: { protocol: '3', transport: 'rest', archetype: 'crud', bin: 'world-googlefonts-api',
28
+ resources: [], specSource: 'api/spec/SOURCE.md', description: 'Google Fonts Developer API',
29
+ adoption: { sdks: [], envStems: [] }, hosts: [{ host: 'webfonts.googleapis.com' }, { host: 'www.googleapis.com', pathPattern: '^/webfonts/v1/webfonts$' }],
30
+ endpointEnvNone: 'The application hardcodes this vendor lane host.' },
31
+ };
@@ -0,0 +1,131 @@
1
+ // Google supplies catalog records and keys outside the read-only Fonts API.
2
+ import type { HandlerContext } from '@volter/world-core';
3
+ import { FONT, CATALOG, CSS, FILE, KEY, object, assetKey, cssKey, css2Key, css2Query, googleError } from './shared.ts';
4
+
5
+ // source: https://developers.google.com/fonts/docs/developer_api
6
+ export async function appCredentials(ctx: HandlerContext): Promise<Response> {
7
+ const project = await ctx.tenant('_project', async organization =>
8
+ String((await ctx.create('_project', { name: organization.label, owner: organization.members[0] }, 'project.create')).id));
9
+ const value = async (id: string): Promise<string> => 'AIza' + (await ctx.secret(`googlefonts:application-api-key:${id}`)).slice(0, 35);
10
+ const key = await ctx.accountKey('api-key', KEY, {
11
+ make: async () => {
12
+ const row = await ctx.create(KEY, { _project: project.id, name: ctx.deployment() || 'world' }, 'api_key.create');
13
+ const id = String(row.id), secret = await value(id);
14
+ await ctx.record(KEY, { sha256: ctx.crypto.digest('sha256', secret) }, id);
15
+ return id;
16
+ },
17
+ read: async row => {
18
+ if (row._project !== project.id) return undefined;
19
+ const secret = await value(String(row.id));
20
+ return row.sha256 === ctx.crypto.digest('sha256', secret) ? secret : undefined;
21
+ },
22
+ });
23
+ return Response.json({ api_key: key.secret });
24
+ }
25
+
26
+ // source: https://developers.google.com/fonts/docs/developer_api
27
+ export async function publishCatalog(ctx: HandlerContext): Promise<Response> {
28
+ // There is no Developer API create-font operation. This explicit publisher
29
+ // door imports real vendor-shaped objects and their representations.
30
+ const input = object(ctx.body), catalog = object(input.catalog);
31
+ const fonts = Array.isArray(catalog.items) ? catalog.items.map(object) : [];
32
+ const sheets = Array.isArray(input.stylesheets) ? input.stylesheets.map(object) : [];
33
+ const files = Array.isArray(input.assets) ? input.assets.map(object) : [];
34
+ // CSS2 serves Google's whole library by its query, not only the Developer API items published here.
35
+ const sheets2 = Array.isArray(input.stylesheets2) ? input.stylesheets2.map(object) : [];
36
+ if (catalog.kind !== 'webfonts#webfontList' || fonts.length === 0)
37
+ return googleError(400, 'A webfonts#webfontList catalog is required');
38
+ const names = new Set<string>();
39
+ const strings = (value: unknown): value is string[] => Array.isArray(value) && value.length > 0 &&
40
+ value.every(v => typeof v === 'string' && v.length > 0) && new Set(value).size === value.length;
41
+ const decoded = new Map<string, { url: string; contentType: string; bytes: Uint8Array; hash: string }>();
42
+ try {
43
+ for (const file of files) {
44
+ if (typeof file.url !== 'string' || typeof file.contentType !== 'string' ||
45
+ !/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+(?:;[^\r\n]*)?$/.test(file.contentType) ||
46
+ typeof file.base64 !== 'string' || !file.base64 ||
47
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.base64))
48
+ throw new Error('Each asset needs its gstatic URL, Content-Type and complete base64 bytes');
49
+ const key = assetKey(file.url);
50
+ if (!/^\/s\/[^?]+$|^\/l\/font\?/.test(key) || decoded.has(key)) throw new Error('Invalid or duplicate asset URL');
51
+ const bytes = Uint8Array.from(atob(file.base64), c => c.charCodeAt(0));
52
+ if (bytes.length === 0) throw new Error('Font bytes must not be empty');
53
+ decoded.set(key, { url: file.url, contentType: file.contentType, bytes, hash: ctx.crypto.digest('sha256', bytes) });
54
+ }
55
+ for (const font of fonts) {
56
+ if (typeof font.family !== 'string' || !font.family || names.has(font.family) ||
57
+ font.kind !== 'webfonts#webfont' || !strings(font.variants) || !strings(font.subsets) ||
58
+ typeof font.category !== 'string' || typeof font.version !== 'string' ||
59
+ typeof font.lastModified !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(font.lastModified))
60
+ throw new Error('Invalid or duplicate vendor font item');
61
+ names.add(font.family);
62
+ const fileMap = object(font.files);
63
+ for (const variant of font.variants) {
64
+ if (typeof fileMap[variant] !== 'string' || !decoded.has(assetKey(fileMap[variant] as string)))
65
+ throw new Error('Every advertised variant file must have a genuine supplied blob');
66
+ }
67
+ if (font.menu !== undefined && (typeof font.menu !== 'string' || !decoded.has(assetKey(font.menu))))
68
+ throw new Error('An advertised menu URL must have a supplied blob');
69
+ }
70
+ const representations = new Set<string>();
71
+ for (const sheet of sheets) {
72
+ if (typeof sheet.family !== 'string' || typeof sheet.subset !== 'string' ||
73
+ typeof sheet.userAgent !== 'string' || !sheet.userAgent || typeof sheet.css !== 'string' ||
74
+ !sheet.css.includes('@font-face')) throw new Error('Invalid stylesheet representation');
75
+ const split = sheet.family.lastIndexOf(':');
76
+ const family = split < 0 ? sheet.family : sheet.family.slice(0, split);
77
+ const variant = split < 0 ? 'regular' : sheet.family.slice(split + 1);
78
+ const font = fonts.find(f => f.family === family);
79
+ if (!font || !(font.variants as string[]).includes(variant) ||
80
+ sheet.subset.split(',').some(s => !(font.subsets as string[]).includes(s)))
81
+ throw new Error('Stylesheet request must name a published font, variant and subset');
82
+ const key = cssKey(ctx, sheet.family, sheet.subset, sheet.userAgent);
83
+ if (representations.has(key)) throw new Error('Duplicate stylesheet request');
84
+ representations.add(key);
85
+ const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]!);
86
+ if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
87
+ throw new Error('Every stylesheet src URL must have a supplied blob');
88
+ }
89
+ for (const sheet of sheets2) {
90
+ const pairs = typeof sheet.query === 'string' ? css2Query(new URLSearchParams(sheet.query)) : undefined;
91
+ if (!pairs || typeof sheet.userAgent !== 'string' || typeof sheet.css !== 'string' || !sheet.css.includes('@font-face'))
92
+ throw new Error('Invalid CSS2 stylesheet representation');
93
+ const key = css2Key(ctx, pairs, sheet.userAgent);
94
+ if (representations.has(key)) throw new Error('Duplicate stylesheet request');
95
+ representations.add(key);
96
+ const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]!);
97
+ if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
98
+ throw new Error('Every stylesheet src URL must have a supplied blob');
99
+ }
100
+ // A content-addressed font URL keeps its bytes across publications.
101
+ for (const [key, file] of decoded) {
102
+ const prior = ctx.row(FILE, key);
103
+ if (prior && prior.sha256 !== file.hash) return googleError(409, 'Published font URL already names other bytes', 'ALREADY_EXISTS');
104
+ }
105
+ } catch (cause) { return googleError(400, cause instanceof Error ? cause.message : 'Invalid catalog publication'); }
106
+ const fingerprint = ctx.crypto.digest('sha256', JSON.stringify(input));
107
+ if (ctx.row(CATALOG, 'published')?.fingerprint === fingerprint) return Response.json({ families: [...names] });
108
+ // Validation precedes all writes. Publish the catalog pointer last: readers
109
+ // select only this generation's metadata and stylesheet records.
110
+ for (const [key, file] of decoded) {
111
+ const blob = 'font-' + file.hash;
112
+ await ctx.blobs.put(blob, file.bytes);
113
+ await ctx.record(FILE, { url: file.url, contentType: file.contentType, sha256: file.hash, blob }, key);
114
+ }
115
+ // A font item is the vendor's resource, not bookkeeping: written as the publication that made it.
116
+ for (const font of fonts) await ctx.write(FONT, fingerprint + ':' + font.family, font, 'webfont.publish');
117
+ for (const sheet of sheets) {
118
+ const key = cssKey(ctx, sheet.family as string, sheet.subset as string, sheet.userAgent as string);
119
+ const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css as string);
120
+ await ctx.blobs.put(blob, new TextEncoder().encode(sheet.css as string));
121
+ await ctx.record(CSS, { family: sheet.family, subset: sheet.subset, userAgent: sheet.userAgent, blob }, fingerprint + ':' + key);
122
+ }
123
+ for (const sheet of sheets2) {
124
+ const key = css2Key(ctx, css2Query(new URLSearchParams(sheet.query as string))!, sheet.userAgent as string);
125
+ const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css as string);
126
+ await ctx.blobs.put(blob, new TextEncoder().encode(sheet.css as string));
127
+ await ctx.record(CSS, { query: sheet.query, userAgent: sheet.userAgent, blob }, fingerprint + ':' + key);
128
+ }
129
+ await ctx.record(CATALOG, { families: [...names], fingerprint }, 'published');
130
+ return Response.json({ families: [...names] }, { status: 201 });
131
+ }
@@ -0,0 +1,8 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+ import * as webfonts from './webfonts.ts';
5
+
6
+ export const apiSemantics: Record<string, Handler> = {
7
+ "webfonts.webfonts.list": webfonts.webfonts_webfonts_list,
8
+ };
@@ -0,0 +1 @@
1
+ export * from '../../../src/semantics/shared.ts';
@@ -0,0 +1,6 @@
1
+ // The catalog and its immutable representations have no status transition.
2
+ // Keys are issued once and stored as hashes; this scope has no key revocation API.
3
+ export const states = {
4
+ webfont: { notState: ['family', 'variants', 'subsets', 'category', 'version', 'lastModified'] },
5
+ _api_key: { notState: ['sha256'] },
6
+ };
@@ -0,0 +1,32 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { FONT, CATALOG, googleError } from './shared.ts';
3
+
4
+ // source: spec:webfonts.webfonts.list "Retrieves the list of fonts currently served by the Google Fonts Developer API."
5
+ export async function webfonts_webfonts_list(ctx: HandlerContext): Promise<Response> {
6
+ const query = new URL(ctx.call.request.url).searchParams;
7
+ // Where this scope stops: the demand selects the default static-TTF catalog.
8
+ // VF/WOFF2/FAMILY_TAGS need an independently published capability snapshot.
9
+ if (query.getAll('capability').some(c => c && c !== 'CAPABILITY_UNSPECIFIED'))
10
+ return googleError(400, 'This published catalog does not provide the requested capability');
11
+ const published = ctx.row(CATALOG, 'published');
12
+ const names = Array.isArray(published?.families) ? published.families : [];
13
+ let items = names.flatMap(family => {
14
+ const row = ctx.row(FONT, String(published?.fingerprint) + ':' + String(family));
15
+ return row ? [ctx.own(row)] : [];
16
+ });
17
+ for (const field of ['family', 'subset', 'category']) {
18
+ const asked = query.getAll(field);
19
+ if (asked.length) items = items.filter(item => asked.some(value => field === 'subset'
20
+ ? Array.isArray(item.subsets) && item.subsets.includes(value) : item[field] === value));
21
+ }
22
+ // source: https://developers.google.com/fonts/docs/developer_api
23
+ const sort = (query.get('sort') ?? 'alpha').toLowerCase();
24
+ if (!['sort_undefined', 'alpha', 'date', 'popularity', 'style', 'trending'].includes(sort))
25
+ return googleError(400, 'Invalid sort value');
26
+ if (sort === 'alpha' || sort === 'sort_undefined') items.sort((a,b) => String(a.family).localeCompare(String(b.family), 'en'));
27
+ else if (sort === 'date') items.sort((a,b) => String(b.lastModified).localeCompare(String(a.lastModified)));
28
+ else if (sort === 'style') items.sort((a,b) => (Array.isArray(b.variants) ? b.variants.length : 0) - (Array.isArray(a.variants) ? a.variants.length : 0));
29
+ // Popularity/trending retain the publisher's catalog order. The reference
30
+ // snapshot is one actual font, not a manufactured global popularity ranking.
31
+ return Response.json({ kind: 'webfonts#webfontList', items });
32
+ }
@@ -0,0 +1,11 @@
1
+ # Google Fonts font files
2
+
3
+ Google's CSS API guide https://developers.google.com/fonts/docs/getting_started
4
+ describes the stylesheet link and font selection. The retained vendor responses
5
+ for the exact application user agents (vendor/inputs.json) bind each actual src URL
6
+ to its font file; EOT and SVG use /l/font with kit/skey/v, the other representations
7
+ use /s/<family>/<version>/<file>. All bytes and Content-Type values are unchanged
8
+ vendor answers, retained under their source hashes. No format conversion occurs.
9
+ Only GET is demanded; unsupported operations remain gaps.
10
+
11
+ SHA-256 `openapi.json`: a008d26adacbeaa5027767e9fa6eadce49ef5b8bdefcb6aa8f3ed9ee07347ac5
@@ -0,0 +1,162 @@
1
+ {
2
+ "openapi": "3.0.3",
3
+ "info": {
4
+ "title": "Google Fonts font-file hosts",
5
+ "version": "2026-10-12"
6
+ },
7
+ "servers": [
8
+ {
9
+ "url": "https://fonts.gstatic.com"
10
+ }
11
+ ],
12
+ "paths": {
13
+ "/s/{file}": {
14
+ "get": {
15
+ "operationId": "getFontFile",
16
+ "description": "Download the font-file bytes named by a Google Fonts stylesheet src URL.",
17
+ "parameters": [
18
+ {
19
+ "name": "file",
20
+ "in": "path",
21
+ "required": true,
22
+ "schema": {
23
+ "type": "string"
24
+ }
25
+ }
26
+ ],
27
+ "responses": {
28
+ "200": {
29
+ "description": "The font-file bytes at the returned stylesheet URL",
30
+ "content": {
31
+ "font/eot": {
32
+ "schema": {
33
+ "type": "string",
34
+ "format": "binary"
35
+ }
36
+ },
37
+ "font/ttf": {
38
+ "schema": {
39
+ "type": "string",
40
+ "format": "binary"
41
+ }
42
+ },
43
+ "font/woff": {
44
+ "schema": {
45
+ "type": "string",
46
+ "format": "binary"
47
+ }
48
+ },
49
+ "font/woff2": {
50
+ "schema": {
51
+ "type": "string",
52
+ "format": "binary"
53
+ }
54
+ },
55
+ "image/svg+xml": {
56
+ "schema": {
57
+ "type": "string",
58
+ "format": "binary"
59
+ }
60
+ }
61
+ }
62
+ },
63
+ "404": {
64
+ "description": "Font file not found",
65
+ "content": {
66
+ "text/html": {
67
+ "schema": {
68
+ "type": "string"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ },
74
+ "tags": [
75
+ "files"
76
+ ]
77
+ }
78
+ },
79
+ "/l/font": {
80
+ "get": {
81
+ "operationId": "getLegacyFontFile",
82
+ "description": "Download the font-file bytes named by a Google Fonts stylesheet src URL for a legacy browser.",
83
+ "parameters": [
84
+ {
85
+ "name": "kit",
86
+ "in": "query",
87
+ "required": true,
88
+ "schema": {
89
+ "type": "string"
90
+ }
91
+ },
92
+ {
93
+ "name": "skey",
94
+ "in": "query",
95
+ "required": true,
96
+ "schema": {
97
+ "type": "string"
98
+ }
99
+ },
100
+ {
101
+ "name": "v",
102
+ "in": "query",
103
+ "required": true,
104
+ "schema": {
105
+ "type": "string"
106
+ }
107
+ }
108
+ ],
109
+ "responses": {
110
+ "200": {
111
+ "description": "The font-file bytes at the returned stylesheet URL",
112
+ "content": {
113
+ "font/eot": {
114
+ "schema": {
115
+ "type": "string",
116
+ "format": "binary"
117
+ }
118
+ },
119
+ "font/ttf": {
120
+ "schema": {
121
+ "type": "string",
122
+ "format": "binary"
123
+ }
124
+ },
125
+ "font/woff": {
126
+ "schema": {
127
+ "type": "string",
128
+ "format": "binary"
129
+ }
130
+ },
131
+ "font/woff2": {
132
+ "schema": {
133
+ "type": "string",
134
+ "format": "binary"
135
+ }
136
+ },
137
+ "image/svg+xml": {
138
+ "schema": {
139
+ "type": "string",
140
+ "format": "binary"
141
+ }
142
+ }
143
+ }
144
+ },
145
+ "404": {
146
+ "description": "Font file not found",
147
+ "content": {
148
+ "text/html": {
149
+ "schema": {
150
+ "type": "string"
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ "tags": [
157
+ "files"
158
+ ]
159
+ }
160
+ }
161
+ }
162
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,14 @@
1
+ // GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
2
+ // the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
3
+ import { createPackFetch, type DerivedFetch, type DerivedSurface, type PackFetchOptions } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from './manifest.ts';
6
+ import { assetsSemantics } from './semantics/index.ts';
7
+
8
+ export function createAssetsLaneFetch(options: PackFetchOptions = {}): DerivedFetch {
9
+ return createPackFetch({
10
+ manifest,
11
+ surface: surface as DerivedSurface,
12
+ handlers: assetsSemantics,
13
+ }, options);
14
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"openapi.json","format":"openapi","version":"2026-10-12","resources":[],"operations":[{"id":"getLegacyFontFile","method":"get","path":"/l/font","pathParams":[],"query":[{"name":"kit","type":"string","required":true},{"name":"skey","type":"string","required":true},{"name":"v","type":"string","required":true}],"body":[],"bodyEncoding":"none","successStatus":200,"responseMedia":{"200":["image/svg+xml"]},"class":"computed"},{"id":"getFontFile","method":"get","path":"/s/{file}","pathParams":["file"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"responseMedia":{"200":["image/svg+xml"]},"class":"computed"}]}
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{}}
@@ -0,0 +1,3 @@
1
+ // The assets lane of the googlefonts pack: the vendor's manifest routes its requests here.
2
+ export { createAssetsLaneFetch } from './fetch.ts';
3
+ export { manifest } from './manifest.ts';
@@ -0,0 +1,19 @@
1
+ // Vendor wire facts; the surface is derived from the retained spec.
2
+ import type { DerivedManifest } from '@volter/world-core';
3
+ export const manifest: DerivedManifest = {
4
+ vendor: 'googlefonts', service: 'googlefonts', body: { json: 'always' },
5
+ ids: { template: '{uuid}' }, time: 'iso',
6
+ error: { error: { code: '{status}', message: '{message}', status: '{kind}' } },
7
+ readOnly: { status: 403, kind: 'PERMISSION_DENIED', message: 'Read-only World' },
8
+ notFound: { status: 404, kind: 'NOT_FOUND', message: 'Not Found' },
9
+ gap: { status: 404, kind: 'NOT_FOUND', message: 'Not Found' },
10
+ list: { style: 'envelope', envelope: { items: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
11
+ deleted: {},
12
+ resources: {},
13
+ discovery: { twinOf: 'Google Fonts font bytes', stores: 'Published catalog and font representations in the shared googlefonts store' },
14
+ spanning: ['file'],
15
+ descriptor: { protocol: '3', transport: 'rest', archetype: 'crud', bin: 'world-googlefonts-assets',
16
+ resources: [], specSource: 'assets/spec/SOURCE.md', description: 'Google Fonts font bytes',
17
+ adoption: { sdks: [], envStems: [] }, hosts: [{ host: 'fonts.gstatic.com', pathPattern: '^/(s/|l/font$)' }],
18
+ endpointEnvNone: 'The application hardcodes this vendor lane host.' },
19
+ };
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+ import * as l from './l.ts';
5
+ import * as s from './s.ts';
6
+
7
+ export const assetsSemantics: Record<string, Handler> = {
8
+ getFontFile: s.getFontFile,
9
+ getLegacyFontFile: l.getLegacyFontFile,
10
+ };
@@ -0,0 +1,4 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { downloadFont } from './shared.ts';
3
+ // source: spec:getLegacyFontFile "Download the font-file bytes named by a Google Fonts stylesheet src URL for a legacy browser."
4
+ export async function getLegacyFontFile(ctx: HandlerContext): Promise<Response> { return downloadFont(ctx); }
@@ -0,0 +1,4 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { downloadFont } from './shared.ts';
3
+ // source: spec:getFontFile "Download the font-file bytes named by a Google Fonts stylesheet src URL."
4
+ export async function getFontFile(ctx: HandlerContext): Promise<Response> { return downloadFont(ctx); }
@@ -0,0 +1,13 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { FILE, assetKey, htmlError } from '../../../src/semantics/shared.ts';
3
+
4
+ export async function downloadFont(ctx: HandlerContext): Promise<Response> {
5
+ const request = new URL(ctx.call.request.url);
6
+ const vendorUrl = new URL(request.pathname + request.search, 'https://fonts.gstatic.com');
7
+ const row = ctx.row(FILE, assetKey(vendorUrl.href));
8
+ if (!row) return htmlError(404, 'Font file not found');
9
+ const body = await ctx.blobs.get(String(row.blob));
10
+ if (!body) return htmlError(404, 'Font file not found');
11
+ return new Response(Uint8Array.from(body).buffer, { headers: { 'content-type': String(row.contentType), 'content-length': String(body.length),
12
+ 'access-control-allow-origin': '*', 'cache-control': 'public, max-age=31536000, immutable' } });
13
+ }
@@ -0,0 +1,15 @@
1
+ # Google Fonts CSS API v1 and CSS2
2
+
3
+ This contract is written from Google's https://developers.google.com/fonts/docs/getting_started,
4
+ https://developers.google.com/fonts/docs/css2 and the retained responses in vendor/inputs.json,
5
+ vendor/inter.json and vendor/noto-sans-sc-text.json. google-webfonts-helper calls
6
+ GET /css?family=<family>:<variant>&subset=<subsets> with the exact configured
7
+ User-Agent and consumes each @font-face's font-family, style, weight and src URL.
8
+ Dub's Open Graph routes call GET /css2?family=Inter:wght@<weight> and Rallly's
9
+ calls GET /css2?family=Noto+Sans+SC:wght@<weight>&text=<text>, both from a server
10
+ without a browser User-Agent, and read the truetype src URL.
11
+ The supplied representations retain actual vendor CSS. Requests without a
12
+ published representation answer a refusal, never an invented font format.
13
+ text/display/effect on /css are outside this client's demand.
14
+
15
+ SHA-256 `openapi.json`: bf7b9a3b70c3a75712fb784e22bfe07c0008dd55e1aa93b31cba8da26a64fb60
@@ -0,0 +1,142 @@
1
+ {
2
+ "openapi": "3.0.3",
3
+ "info": {
4
+ "title": "Google Fonts CSS API",
5
+ "version": "v1"
6
+ },
7
+ "servers": [
8
+ {
9
+ "url": "https://fonts.googleapis.com"
10
+ }
11
+ ],
12
+ "paths": {
13
+ "/css": {
14
+ "get": {
15
+ "operationId": "getStylesheet",
16
+ "description": "The Google Fonts API provides the regular version of the requested fonts by default. To request other styles or weights, append a colon to the name of the font. The subset parameter specifies script subsets. The representation is selected for the requesting browser.",
17
+ "parameters": [
18
+ {
19
+ "name": "family",
20
+ "in": "query",
21
+ "required": true,
22
+ "schema": {
23
+ "type": "string"
24
+ }
25
+ },
26
+ {
27
+ "name": "subset",
28
+ "in": "query",
29
+ "required": false,
30
+ "schema": {
31
+ "type": "string"
32
+ }
33
+ },
34
+ {
35
+ "name": "User-Agent",
36
+ "in": "header",
37
+ "schema": {
38
+ "type": "string"
39
+ }
40
+ }
41
+ ],
42
+ "responses": {
43
+ "200": {
44
+ "description": "A stylesheet referring to font-file URLs",
45
+ "content": {
46
+ "text/css": {
47
+ "schema": {
48
+ "type": "string"
49
+ }
50
+ }
51
+ }
52
+ },
53
+ "400": {
54
+ "description": "Bad font request",
55
+ "content": {
56
+ "text/html": {
57
+ "schema": {
58
+ "type": "string"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "/css2": {
67
+ "get": {
68
+ "operationId": "getStylesheet2",
69
+ "description": "It all starts with a new base URL: https://fonts.googleapis.com/css2 The syntax for specifying styles within each family has changed to describe variable font ‘design spaces.’ To request multiple families, specify the family= param for each family. Please note that the 'text=' parameter can only be specified once. It applies to all families in the request. As a general note, the updated CSS API is more strict about what requests are accepted than the original CSS API. The representation is selected for the requesting browser.",
70
+ "parameters": [
71
+ {
72
+ "name": "family",
73
+ "in": "query",
74
+ "required": true,
75
+ "style": "form",
76
+ "explode": true,
77
+ "description": "<family_name>[:<axis_tag_list>@<axis_tuple_list>]",
78
+ "schema": {
79
+ "type": "array",
80
+ "items": {
81
+ "type": "string"
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "name": "text",
87
+ "in": "query",
88
+ "required": false,
89
+ "description": "The text that will be displayed in the requested typeface",
90
+ "schema": {
91
+ "type": "string"
92
+ }
93
+ },
94
+ {
95
+ "name": "display",
96
+ "in": "query",
97
+ "required": false,
98
+ "schema": {
99
+ "type": "string",
100
+ "enum": [
101
+ "auto",
102
+ "block",
103
+ "swap",
104
+ "fallback",
105
+ "optional"
106
+ ]
107
+ }
108
+ },
109
+ {
110
+ "name": "User-Agent",
111
+ "in": "header",
112
+ "schema": {
113
+ "type": "string"
114
+ }
115
+ }
116
+ ],
117
+ "responses": {
118
+ "200": {
119
+ "description": "A stylesheet referring to font-file URLs",
120
+ "content": {
121
+ "text/css": {
122
+ "schema": {
123
+ "type": "string"
124
+ }
125
+ }
126
+ }
127
+ },
128
+ "400": {
129
+ "description": "Bad font request",
130
+ "content": {
131
+ "text/html": {
132
+ "schema": {
133
+ "type": "string"
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
@@ -0,0 +1 @@
1
+ {}