@volter/twin-googlefonts 1.0.1 → 1.0.3

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 (70) hide show
  1. package/api/spec/sources.json +11 -1
  2. package/api/src/semantics/doors.ts +10 -5
  3. package/api/src/semantics/index.ts +1 -1
  4. package/assets/src/semantics/index.ts +1 -1
  5. package/css/spec/SOURCE.md +10 -2
  6. package/css/src/semantics/css.ts +5 -3
  7. package/css/src/semantics/css2.ts +9 -8
  8. package/css/src/semantics/index.ts +1 -1
  9. package/dist/api/spec/sources.json +11 -1
  10. package/dist/api/src/semantics/doors.js +10 -5
  11. package/dist/css/spec/SOURCE.md +10 -2
  12. package/dist/css/src/semantics/css.js +5 -3
  13. package/dist/css/src/semantics/css2.js +9 -8
  14. package/dist/generated/pack-facts.json +68 -0
  15. package/dist/journeys/customer-life.json +81 -0
  16. package/dist/journeys/decisions.json +15 -5
  17. package/dist/journeys/demand.json +56 -0
  18. package/dist/src/cli.js +11 -10
  19. package/dist/src/manifest.js +5 -3
  20. package/dist/src/semantics/seed.js +135 -0
  21. package/dist/src/semantics/shared.d.ts +1 -0
  22. package/dist/src/semantics/shared.js +50 -2
  23. package/generated/pack-facts.json +5 -2
  24. package/journeys/customer-life.json +81 -0
  25. package/journeys/decisions.json +15 -5
  26. package/journeys/demand.json +56 -0
  27. package/package.json +30 -14
  28. package/src/cli.ts +10 -10
  29. package/src/manifest.ts +5 -3
  30. package/src/semantics/seed.ts +135 -0
  31. package/src/semantics/shared.ts +55 -2
  32. package/{dist/vendor/OFL.txt → vendor/DMMono-OFL.txt} +93 -94
  33. package/vendor/SOURCE.md +28 -0
  34. package/vendor/browser-classes.json +529 -0
  35. package/vendor/inter-dm-mono-chromium.json +108 -0
  36. package/vendor/inter-variable.json +52 -0
  37. package/dist/api/src/fetch.ts +0 -16
  38. package/dist/api/src/index.ts +0 -3
  39. package/dist/api/src/manifest.ts +0 -31
  40. package/dist/api/src/semantics/doors.ts +0 -131
  41. package/dist/api/src/semantics/index.ts +0 -8
  42. package/dist/api/src/semantics/shared.ts +0 -1
  43. package/dist/api/src/semantics/states.ts +0 -6
  44. package/dist/api/src/semantics/webfonts.ts +0 -32
  45. package/dist/assets/src/fetch.ts +0 -14
  46. package/dist/assets/src/index.ts +0 -3
  47. package/dist/assets/src/manifest.ts +0 -19
  48. package/dist/assets/src/semantics/index.ts +0 -10
  49. package/dist/assets/src/semantics/l.ts +0 -4
  50. package/dist/assets/src/semantics/s.ts +0 -4
  51. package/dist/assets/src/semantics/shared.ts +0 -13
  52. package/dist/css/src/fetch.ts +0 -14
  53. package/dist/css/src/index.ts +0 -3
  54. package/dist/css/src/manifest.ts +0 -19
  55. package/dist/css/src/semantics/css.ts +0 -19
  56. package/dist/css/src/semantics/css2.ts +0 -20
  57. package/dist/css/src/semantics/index.ts +0 -10
  58. package/dist/css/src/semantics/shared.ts +0 -1
  59. package/dist/journeys/customer-life.outline.md +0 -24
  60. package/dist/journeys/customer-life.outline.verdict.json +0 -82
  61. package/dist/journeys/customer-life.verdict.json +0 -149
  62. package/dist/vendor/Inter-OFL.txt +0 -93
  63. package/dist/vendor/SOURCE.md +0 -33
  64. package/dist/vendor/antic.json +0 -93
  65. package/dist/vendor/inputs.json +0 -460
  66. package/dist/vendor/inter.json +0 -44
  67. package/dist/vendor/noto-sans-sc-text.json +0 -24
  68. package/journeys/customer-life.outline.md +0 -24
  69. package/journeys/customer-life.outline.verdict.json +0 -82
  70. package/journeys/customer-life.verdict.json +0 -149
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "https://developers.google.com/fonts/docs/developer_api": {
3
3
  "status": 200,
4
- "checked": "2026-10-12"
4
+ "checked": "2026-11-11",
5
+ "quotes": {
6
+ "by including an API key with each request": true
7
+ }
8
+ },
9
+ "https://docs.cloud.google.com/docs/authentication/api-keys": {
10
+ "status": 200,
11
+ "checked": "2026-11-11",
12
+ "quotes": {
13
+ "Standard API keys provide a way to associate a request with a project for billing and quota purposes.": true
14
+ }
5
15
  }
6
16
  }
@@ -1,9 +1,13 @@
1
1
  // Google supplies catalog records and keys outside the read-only Fonts API.
2
2
  import type { HandlerContext } from '@volter/world-core';
3
- import { FONT, CATALOG, CSS, FILE, KEY, object, assetKey, cssKey, css2Key, css2Query, googleError } from './shared.ts';
3
+ import { FONT, CATALOG, CSS, FILE, KEY, object, assetKey, browserClass, cssKey, css2Key, css2Query, googleError } from './shared.ts';
4
4
 
5
5
  // source: https://developers.google.com/fonts/docs/developer_api
6
6
  export async function appCredentials(ctx: HandlerContext): Promise<Response> {
7
+ // An app's key is a standard API key, made on a Google Cloud project's Credentials page and associating each request
8
+ // with that project for billing and quota; the Fonts Developer API takes it as `key=` and nothing else.
9
+ // source: https://docs.cloud.google.com/docs/authentication/api-keys "Standard API keys provide a way to associate a request with a project for billing and quota purposes."
10
+ // source: https://developers.google.com/fonts/docs/developer_api "by including an API key with each request"
7
11
  const project = await ctx.tenant('_project', async organization =>
8
12
  String((await ctx.create('_project', { name: organization.label, owner: organization.members[0] }, 'project.create')).id));
9
13
  const value = async (id: string): Promise<string> => 'AIza' + (await ctx.secret(`googlefonts:application-api-key:${id}`)).slice(0, 35);
@@ -79,8 +83,9 @@ export async function publishCatalog(ctx: HandlerContext): Promise<Response> {
79
83
  if (!font || !(font.variants as string[]).includes(variant) ||
80
84
  sheet.subset.split(',').some(s => !(font.subsets as string[]).includes(s)))
81
85
  throw new Error('Stylesheet request must name a published font, variant and subset');
86
+ // a stylesheet is one browser class's representation (shared.ts browserClass): its user agent is that class's sample
82
87
  const key = cssKey(ctx, sheet.family, sheet.subset, sheet.userAgent);
83
- if (representations.has(key)) throw new Error('Duplicate stylesheet request');
88
+ if (representations.has(key)) throw new Error('Duplicate stylesheet request for browser class ' + browserClass(sheet.userAgent));
84
89
  representations.add(key);
85
90
  const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]!);
86
91
  if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
@@ -91,7 +96,7 @@ export async function publishCatalog(ctx: HandlerContext): Promise<Response> {
91
96
  if (!pairs || typeof sheet.userAgent !== 'string' || typeof sheet.css !== 'string' || !sheet.css.includes('@font-face'))
92
97
  throw new Error('Invalid CSS2 stylesheet representation');
93
98
  const key = css2Key(ctx, pairs, sheet.userAgent);
94
- if (representations.has(key)) throw new Error('Duplicate stylesheet request');
99
+ if (representations.has(key)) throw new Error('Duplicate stylesheet request for browser class ' + browserClass(sheet.userAgent));
95
100
  representations.add(key);
96
101
  const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]!);
97
102
  if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
@@ -118,13 +123,13 @@ export async function publishCatalog(ctx: HandlerContext): Promise<Response> {
118
123
  const key = cssKey(ctx, sheet.family as string, sheet.subset as string, sheet.userAgent as string);
119
124
  const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css as string);
120
125
  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);
126
+ await ctx.record(CSS, { family: sheet.family, subset: sheet.subset, userAgent: sheet.userAgent, browserClass: browserClass(sheet.userAgent as string), blob }, fingerprint + ':' + key);
122
127
  }
123
128
  for (const sheet of sheets2) {
124
129
  const key = css2Key(ctx, css2Query(new URLSearchParams(sheet.query as string))!, sheet.userAgent as string);
125
130
  const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css as string);
126
131
  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);
132
+ await ctx.record(CSS, { query: sheet.query, userAgent: sheet.userAgent, browserClass: browserClass(sheet.userAgent as string), blob }, fingerprint + ':' + key);
128
133
  }
129
134
  await ctx.record(CATALOG, { families: [...names], fingerprint }, 'published');
130
135
  return Response.json({ families: [...names] }, { status: 201 });
@@ -1,4 +1,4 @@
1
- // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
1
+ // GENERATED by twin-standard create --index: the handler map, by operationId, from the families' files (each
2
2
  // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
3
  import type { Handler } from '@volter/world-core';
4
4
  import * as webfonts from './webfonts.ts';
@@ -1,4 +1,4 @@
1
- // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
1
+ // GENERATED by twin-standard create --index: the handler map, by operationId, from the families' files (each
2
2
  // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
3
  import type { Handler } from '@volter/world-core';
4
4
  import * as l from './l.ts';
@@ -7,9 +7,17 @@ GET /css?family=<family>:<variant>&subset=<subsets> with the exact configured
7
7
  User-Agent and consumes each @font-face's font-family, style, weight and src URL.
8
8
  Dub's Open Graph routes call GET /css2?family=Inter:wght@<weight> and Rallly's
9
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.
10
+ without a browser User-Agent, and read the truetype src URL. next/font/google's build (Rallly's and
11
+ Cal.diy's `next build`) calls GET /css2?family=Inter:wght@100..900&display=swap with a Chrome User-Agent and
12
+ reads each woff2 src URL of its unicode-range faces.
13
+ Twenty's front end (twentyhq/twenty 323e664, packages/twenty-front/index.html:33,37) links
14
+ GET /css2?family=Inter:wght@400;500;600&display=swap and GET /css2?family=DM+Mono:wght@400;500;600&display=swap
15
+ from the browser, which loads the woff2 faces its text needs.
16
+ A representation is published for a browser class (src/semantics/shared.ts browserClass): Google serves any
17
+ browser, choosing WOFF 2.0 and unicode-range subsets by the browser its User-Agent names, and the classes are
18
+ the agents Google answered the same bytes (vendor/browser-classes.json).
11
19
  The supplied representations retain actual vendor CSS. Requests without a
12
- published representation answer a refusal, never an invented font format.
20
+ representation published for their class answer a refusal, never an invented font format.
13
21
  text/display/effect on /css are outside this client's demand.
14
22
 
15
23
  SHA-256 `openapi.json`: bf7b9a3b70c3a75712fb784e22bfe07c0008dd55e1aa93b31cba8da26a64fb60
@@ -1,5 +1,5 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- import { CATALOG, CSS, cssKey, htmlError } from './shared.ts';
2
+ import { browserClass, CATALOG, CSS, cssKey, htmlError } from './shared.ts';
3
3
 
4
4
  // source: spec:getStylesheet "The representation is selected for the requesting browser."
5
5
  export async function getStylesheet(ctx: HandlerContext): Promise<Response> {
@@ -8,11 +8,13 @@ export async function getStylesheet(ctx: HandlerContext): Promise<Response> {
8
8
  if (!family || [...query.keys()].some(k => !['family','subset'].includes(k)))
9
9
  return htmlError(400, 'Bad font request');
10
10
  const generation = ctx.row(CATALOG, 'published')?.fingerprint;
11
- const key = cssKey(ctx, family, subset, ctx.call.request.headers.get('user-agent') ?? '');
11
+ // the representation published for the browser class the agent belongs to (shared.ts browserClass)
12
+ const userAgent = ctx.call.request.headers.get('user-agent') ?? '';
13
+ const key = cssKey(ctx, family, subset, userAgent);
12
14
  const row = generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + key);
13
15
  // Where the documentation stops: no supplied representation is a named
14
16
  // refusal, not an invented conversion or another browser's font format.
15
- if (!row) return htmlError(400, 'Font family or browser representation is not published');
17
+ if (!row) return htmlError(400, 'Font family or browser representation is not published (' + browserClass(userAgent) + ')');
16
18
  const body = await ctx.blobs.get(String(row.blob));
17
19
  if (!body) return htmlError(404, 'Font stylesheet not found');
18
20
  return new Response(Uint8Array.from(body).buffer, { headers: { 'content-type': 'text/css; charset=utf-8', 'content-length': String(body.length), 'vary': 'User-Agent' } });
@@ -1,5 +1,5 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- import { CATALOG, CSS, css2Key, css2Query, htmlError } from './shared.ts';
2
+ import { browserClass, CATALOG, CSS, css2Key, css2Query, htmlError } from './shared.ts';
3
3
 
4
4
  // source: spec:getStylesheet2 "To request multiple families, specify the family= param for each family."
5
5
  export async function getStylesheet2(ctx: HandlerContext): Promise<Response> {
@@ -7,13 +7,14 @@ export async function getStylesheet2(ctx: HandlerContext): Promise<Response> {
7
7
  if (!pairs) return htmlError(400, 'Bad font request');
8
8
  const generation = ctx.row(CATALOG, 'published')?.fingerprint;
9
9
  const userAgent = ctx.call.request.headers.get('user-agent') ?? '';
10
- const sheet = (agent: string) => generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + css2Key(ctx, pairs, agent));
11
- // Where the evidence stops: Google answered the same truetype stylesheet to no User-Agent, "node" and "undici"
12
- // (vendor/inter.json). A request whose agent is not a browser's ("Mozilla/...") and has no representation of its
13
- // own reads the one published for no agent; a browser without its own representation is refused, never sent
14
- // another browser's format.
15
- const row = sheet(userAgent) ?? (userAgent.startsWith('Mozilla/') ? undefined : sheet(''));
16
- if (!row) return htmlError(400, 'Font family or browser representation is not published');
10
+ // The representation is the one published for the browser class the agent belongs to (shared.ts browserClass): a
11
+ // request with no browser's agent ("node", "undici", none) reads the truetype class, as Google answered the same CSS
12
+ // to all three (vendor/inter.json).
13
+ // source: spec:getStylesheet2 "The representation is selected for the requesting browser."
14
+ const row = generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + css2Key(ctx, pairs, userAgent));
15
+ // Where the documentation stops: a class with no published representation is a named refusal, never another
16
+ // class's font format.
17
+ if (!row) return htmlError(400, 'Font family or browser representation is not published (' + browserClass(userAgent) + ')');
17
18
  const body = await ctx.blobs.get(String(row.blob));
18
19
  if (!body) return htmlError(404, 'Font stylesheet not found');
19
20
  return new Response(Uint8Array.from(body).buffer, { headers: { 'content-type': 'text/css; charset=utf-8', 'content-length': String(body.length), 'access-control-allow-origin': '*' } });
@@ -1,4 +1,4 @@
1
- // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
1
+ // GENERATED by twin-standard create --index: the handler map, by operationId, from the families' files (each
2
2
  // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
3
  import type { Handler } from '@volter/world-core';
4
4
  import * as css from './css.ts';
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "https://developers.google.com/fonts/docs/developer_api": {
3
3
  "status": 200,
4
- "checked": "2026-10-12"
4
+ "checked": "2026-11-11",
5
+ "quotes": {
6
+ "by including an API key with each request": true
7
+ }
8
+ },
9
+ "https://docs.cloud.google.com/docs/authentication/api-keys": {
10
+ "status": 200,
11
+ "checked": "2026-11-11",
12
+ "quotes": {
13
+ "Standard API keys provide a way to associate a request with a project for billing and quota purposes.": true
14
+ }
5
15
  }
6
16
  }
@@ -1,6 +1,10 @@
1
- import { FONT, CATALOG, CSS, FILE, KEY, object, assetKey, cssKey, css2Key, css2Query, googleError } from "./shared.js";
1
+ import { FONT, CATALOG, CSS, FILE, KEY, object, assetKey, browserClass, cssKey, css2Key, css2Query, googleError } from "./shared.js";
2
2
  // source: https://developers.google.com/fonts/docs/developer_api
3
3
  export async function appCredentials(ctx) {
4
+ // An app's key is a standard API key, made on a Google Cloud project's Credentials page and associating each request
5
+ // with that project for billing and quota; the Fonts Developer API takes it as `key=` and nothing else.
6
+ // source: https://docs.cloud.google.com/docs/authentication/api-keys "Standard API keys provide a way to associate a request with a project for billing and quota purposes."
7
+ // source: https://developers.google.com/fonts/docs/developer_api "by including an API key with each request"
4
8
  const project = await ctx.tenant('_project', async (organization) => String((await ctx.create('_project', { name: organization.label, owner: organization.members[0] }, 'project.create')).id));
5
9
  const value = async (id) => 'AIza' + (await ctx.secret(`googlefonts:application-api-key:${id}`)).slice(0, 35);
6
10
  const key = await ctx.accountKey('api-key', KEY, {
@@ -78,9 +82,10 @@ export async function publishCatalog(ctx) {
78
82
  if (!font || !font.variants.includes(variant) ||
79
83
  sheet.subset.split(',').some(s => !font.subsets.includes(s)))
80
84
  throw new Error('Stylesheet request must name a published font, variant and subset');
85
+ // a stylesheet is one browser class's representation (shared.ts browserClass): its user agent is that class's sample
81
86
  const key = cssKey(ctx, sheet.family, sheet.subset, sheet.userAgent);
82
87
  if (representations.has(key))
83
- throw new Error('Duplicate stylesheet request');
88
+ throw new Error('Duplicate stylesheet request for browser class ' + browserClass(sheet.userAgent));
84
89
  representations.add(key);
85
90
  const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]);
86
91
  if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
@@ -92,7 +97,7 @@ export async function publishCatalog(ctx) {
92
97
  throw new Error('Invalid CSS2 stylesheet representation');
93
98
  const key = css2Key(ctx, pairs, sheet.userAgent);
94
99
  if (representations.has(key))
95
- throw new Error('Duplicate stylesheet request');
100
+ throw new Error('Duplicate stylesheet request for browser class ' + browserClass(sheet.userAgent));
96
101
  representations.add(key);
97
102
  const urls = [...sheet.css.matchAll(/url\((https?:\/\/[^)]+)\)/g)].map(m => m[1]);
98
103
  if (urls.length === 0 || urls.some(url => !decoded.has(assetKey(url))))
@@ -125,13 +130,13 @@ export async function publishCatalog(ctx) {
125
130
  const key = cssKey(ctx, sheet.family, sheet.subset, sheet.userAgent);
126
131
  const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css);
127
132
  await ctx.blobs.put(blob, new TextEncoder().encode(sheet.css));
128
- await ctx.record(CSS, { family: sheet.family, subset: sheet.subset, userAgent: sheet.userAgent, blob }, fingerprint + ':' + key);
133
+ await ctx.record(CSS, { family: sheet.family, subset: sheet.subset, userAgent: sheet.userAgent, browserClass: browserClass(sheet.userAgent), blob }, fingerprint + ':' + key);
129
134
  }
130
135
  for (const sheet of sheets2) {
131
136
  const key = css2Key(ctx, css2Query(new URLSearchParams(sheet.query)), sheet.userAgent);
132
137
  const blob = 'css-' + ctx.crypto.digest('sha256', sheet.css);
133
138
  await ctx.blobs.put(blob, new TextEncoder().encode(sheet.css));
134
- await ctx.record(CSS, { query: sheet.query, userAgent: sheet.userAgent, blob }, fingerprint + ':' + key);
139
+ await ctx.record(CSS, { query: sheet.query, userAgent: sheet.userAgent, browserClass: browserClass(sheet.userAgent), blob }, fingerprint + ':' + key);
135
140
  }
136
141
  await ctx.record(CATALOG, { families: [...names], fingerprint }, 'published');
137
142
  return Response.json({ families: [...names] }, { status: 201 });
@@ -7,9 +7,17 @@ GET /css?family=<family>:<variant>&subset=<subsets> with the exact configured
7
7
  User-Agent and consumes each @font-face's font-family, style, weight and src URL.
8
8
  Dub's Open Graph routes call GET /css2?family=Inter:wght@<weight> and Rallly's
9
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.
10
+ without a browser User-Agent, and read the truetype src URL. next/font/google's build (Rallly's and
11
+ Cal.diy's `next build`) calls GET /css2?family=Inter:wght@100..900&display=swap with a Chrome User-Agent and
12
+ reads each woff2 src URL of its unicode-range faces.
13
+ Twenty's front end (twentyhq/twenty 323e664, packages/twenty-front/index.html:33,37) links
14
+ GET /css2?family=Inter:wght@400;500;600&display=swap and GET /css2?family=DM+Mono:wght@400;500;600&display=swap
15
+ from the browser, which loads the woff2 faces its text needs.
16
+ A representation is published for a browser class (src/semantics/shared.ts browserClass): Google serves any
17
+ browser, choosing WOFF 2.0 and unicode-range subsets by the browser its User-Agent names, and the classes are
18
+ the agents Google answered the same bytes (vendor/browser-classes.json).
11
19
  The supplied representations retain actual vendor CSS. Requests without a
12
- published representation answer a refusal, never an invented font format.
20
+ representation published for their class answer a refusal, never an invented font format.
13
21
  text/display/effect on /css are outside this client's demand.
14
22
 
15
23
  SHA-256 `openapi.json`: bf7b9a3b70c3a75712fb784e22bfe07c0008dd55e1aa93b31cba8da26a64fb60
@@ -1,4 +1,4 @@
1
- import { CATALOG, CSS, cssKey, htmlError } from "./shared.js";
1
+ import { browserClass, CATALOG, CSS, cssKey, htmlError } from "./shared.js";
2
2
  // source: spec:getStylesheet "The representation is selected for the requesting browser."
3
3
  export async function getStylesheet(ctx) {
4
4
  const query = new URL(ctx.call.request.url).searchParams;
@@ -6,12 +6,14 @@ export async function getStylesheet(ctx) {
6
6
  if (!family || [...query.keys()].some(k => !['family', 'subset'].includes(k)))
7
7
  return htmlError(400, 'Bad font request');
8
8
  const generation = ctx.row(CATALOG, 'published')?.fingerprint;
9
- const key = cssKey(ctx, family, subset, ctx.call.request.headers.get('user-agent') ?? '');
9
+ // the representation published for the browser class the agent belongs to (shared.ts browserClass)
10
+ const userAgent = ctx.call.request.headers.get('user-agent') ?? '';
11
+ const key = cssKey(ctx, family, subset, userAgent);
10
12
  const row = generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + key);
11
13
  // Where the documentation stops: no supplied representation is a named
12
14
  // refusal, not an invented conversion or another browser's font format.
13
15
  if (!row)
14
- return htmlError(400, 'Font family or browser representation is not published');
16
+ return htmlError(400, 'Font family or browser representation is not published (' + browserClass(userAgent) + ')');
15
17
  const body = await ctx.blobs.get(String(row.blob));
16
18
  if (!body)
17
19
  return htmlError(404, 'Font stylesheet not found');
@@ -1,4 +1,4 @@
1
- import { CATALOG, CSS, css2Key, css2Query, htmlError } from "./shared.js";
1
+ import { browserClass, CATALOG, CSS, css2Key, css2Query, htmlError } from "./shared.js";
2
2
  // source: spec:getStylesheet2 "To request multiple families, specify the family= param for each family."
3
3
  export async function getStylesheet2(ctx) {
4
4
  const pairs = css2Query(new URL(ctx.call.request.url).searchParams);
@@ -6,14 +6,15 @@ export async function getStylesheet2(ctx) {
6
6
  return htmlError(400, 'Bad font request');
7
7
  const generation = ctx.row(CATALOG, 'published')?.fingerprint;
8
8
  const userAgent = ctx.call.request.headers.get('user-agent') ?? '';
9
- const sheet = (agent) => generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + css2Key(ctx, pairs, agent));
10
- // Where the evidence stops: Google answered the same truetype stylesheet to no User-Agent, "node" and "undici"
11
- // (vendor/inter.json). A request whose agent is not a browser's ("Mozilla/...") and has no representation of its
12
- // own reads the one published for no agent; a browser without its own representation is refused, never sent
13
- // another browser's format.
14
- const row = sheet(userAgent) ?? (userAgent.startsWith('Mozilla/') ? undefined : sheet(''));
9
+ // The representation is the one published for the browser class the agent belongs to (shared.ts browserClass): a
10
+ // request with no browser's agent ("node", "undici", none) reads the truetype class, as Google answered the same CSS
11
+ // to all three (vendor/inter.json).
12
+ // source: spec:getStylesheet2 "The representation is selected for the requesting browser."
13
+ const row = generation === undefined ? undefined : ctx.row(CSS, String(generation) + ':' + css2Key(ctx, pairs, userAgent));
14
+ // Where the documentation stops: a class with no published representation is a named refusal, never another
15
+ // class's font format.
15
16
  if (!row)
16
- return htmlError(400, 'Font family or browser representation is not published');
17
+ return htmlError(400, 'Font family or browser representation is not published (' + browserClass(userAgent) + ')');
17
18
  const body = await ctx.blobs.get(String(row.blob));
18
19
  if (!body)
19
20
  return htmlError(404, 'Font stylesheet not found');
@@ -0,0 +1,68 @@
1
+ {
2
+ "//": "GENERATED by twin-standard facts from the pack descriptor; no prior-artifact entries are retained.",
3
+ "packs": {
4
+ "googlefonts": {
5
+ "adoption": {
6
+ "sdks": [],
7
+ "envStems": [
8
+ "GOOGLE_FONTS"
9
+ ],
10
+ "modules": [
11
+ "next/font/google"
12
+ ]
13
+ },
14
+ "hosts": [
15
+ {
16
+ "host": "www.googleapis.com",
17
+ "pathPattern": "^/webfonts/v1/webfonts$"
18
+ },
19
+ {
20
+ "host": "webfonts.googleapis.com",
21
+ "pathPattern": "^/v1/webfonts$"
22
+ },
23
+ {
24
+ "host": "fonts.googleapis.com",
25
+ "pathPattern": "^/css2?$"
26
+ },
27
+ {
28
+ "host": "fonts.gstatic.com",
29
+ "pathPattern": "^/(s/|l/font$)"
30
+ }
31
+ ],
32
+ "archetype": "crud",
33
+ "protocol": {
34
+ "declared": "3",
35
+ "major": 3,
36
+ "standing": "current"
37
+ },
38
+ "transport": "rest",
39
+ "resources": [
40
+ "webfont",
41
+ "_api_key",
42
+ "_font_catalog",
43
+ "_stylesheet",
44
+ "_font_file"
45
+ ],
46
+ "endpointEnvNone": "Pinned helper hardcodes the vendor API and CSS hosts, then downloads their returned gstatic URLs. Scoped host routing preserves these URLs.",
47
+ "credentialDoor": {
48
+ "path": "/_twin/app-credentials",
49
+ "body": {},
50
+ "account": {
51
+ "field": "account",
52
+ "tenancy": "Google Cloud project",
53
+ "source": "https://developers.google.com/fonts/docs/developer_api"
54
+ },
55
+ "deployment": {
56
+ "field": "deployment",
57
+ "object": "Project API key",
58
+ "source": "https://docs.cloud.google.com/docs/authentication/api-keys"
59
+ },
60
+ "fill": {
61
+ "GOOGLE_FONTS_API_KEY": "api_key"
62
+ }
63
+ },
64
+ "serveExport": "createGooglefontsTwinServer"
65
+ }
66
+ },
67
+ "undeclared": []
68
+ }