@volter/world-core 3.0.160 → 3.0.162

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 (41) hide show
  1. package/dist/generated/pack-facts.json +190 -6
  2. package/dist/inject.cjs +2 -2
  3. package/dist/src/derived-core.d.ts +9 -1
  4. package/dist/src/derived-core.js +120 -57
  5. package/dist/src/index.d.ts +4 -2
  6. package/dist/src/index.js +3 -1
  7. package/dist/src/live/rtmp.js +3 -0
  8. package/dist/src/machines.d.ts +4 -16
  9. package/dist/src/machines.js +10 -44
  10. package/dist/src/pack-fetch.js +29 -5
  11. package/dist/src/packRegistry.d.ts +7 -0
  12. package/dist/src/protobuf.d.ts +4 -0
  13. package/dist/src/protobuf.js +43 -2
  14. package/dist/src/request-scope.d.ts +2 -0
  15. package/dist/src/request-scope.js +9 -0
  16. package/dist/src/runtime.d.ts +6 -3
  17. package/dist/src/runtime.js +4 -1
  18. package/dist/src/serve-http.d.ts +11 -0
  19. package/dist/src/serve-http.js +132 -0
  20. package/dist/src/signing.d.ts +30 -7
  21. package/dist/src/signing.js +32 -16
  22. package/dist/src/sockets.d.ts +1 -1
  23. package/dist/src/sockets.js +11 -7
  24. package/dist/src/world-account-name.d.ts +2 -0
  25. package/dist/src/world-account-name.js +4 -0
  26. package/generated/pack-facts.json +190 -6
  27. package/inject.cjs +2 -2
  28. package/package.json +9 -1
  29. package/src/derived-core.ts +125 -63
  30. package/src/index.ts +5 -2
  31. package/src/live/rtmp.ts +3 -0
  32. package/src/machines.ts +10 -38
  33. package/src/pack-fetch.ts +27 -5
  34. package/src/packRegistry.ts +2 -2
  35. package/src/protobuf.ts +39 -2
  36. package/src/request-scope.ts +9 -0
  37. package/src/runtime.ts +8 -3
  38. package/src/serve-http.ts +111 -0
  39. package/src/signing.ts +38 -18
  40. package/src/sockets.ts +12 -9
  41. package/src/world-account-name.ts +4 -0
@@ -1,3 +1,4 @@
1
+ import { runWithStorageTenant, storageTenant } from "./request-scope.js";
1
2
  // A vendor's persistent wire (architecture, "Other wires: sockets"): a WebSocket its clients hold open, on which the
2
3
  // vendor sends what happens as it happens (Discord's Gateway: a bot identifies, is told the guilds it is in, and is sent
3
4
  // each message, member and interaction as it lands). The manifest declares the socket (`sockets`); the pack's engine
@@ -14,7 +15,7 @@ const nextSession = new Map();
14
15
  const keyOf = (service, root) => `${service}\0${root ?? ''}`;
15
16
  /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
16
17
  export function openSessions(service, root, socket) {
17
- return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket).map((o) => o.session);
18
+ return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket && o.tenant === storageTenant(service)).map((o) => o.session);
18
19
  }
19
20
  /** What a session awaits that the World's clock has run out (a life's `wait` past a relay's timeout), rejected as the
20
21
  * machine's clock would reject it; called as the vendor serves a request ("A request answered by a session"). */
@@ -39,9 +40,11 @@ export function socketWrite(service, root, write) {
39
40
  return;
40
41
  for (const o of open) {
41
42
  const { engine } = o;
43
+ if (o.tenant !== storageTenant(service))
44
+ continue;
42
45
  if (!engine.write)
43
46
  continue;
44
- o.queue = o.queue.then(async () => engine.write(o.session, write, await o.context())).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
47
+ o.queue = o.queue.then(() => o.run(async () => engine.write(o.session, write, await o.context()))).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
45
48
  }
46
49
  }
47
50
  /** A declared socket path's parameters for a request's path, or undefined when it does not match: a `{name}` segment
@@ -66,7 +69,7 @@ function socketParams(declared, path) {
66
69
  }
67
70
  }
68
71
  /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
69
- export function socketUpgrade(service, root, decls, engines, context, hostOf, pathOf = (request) => new URL(request.url).pathname, decide = async () => undefined) {
72
+ export function socketUpgrade(service, root, decls, engines, context, hostOf, pathOf, decide) {
70
73
  const matchFor = (request) => {
71
74
  const path = pathOf(request).replace(/\/+$/, '') || '/';
72
75
  for (const d of decls) {
@@ -113,18 +116,19 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
113
116
  },
114
117
  decide,
115
118
  };
116
- const o = { session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
119
+ const tenant = storageTenant(service), run = (fn) => runWithStorageTenant(service, tenant, fn);
120
+ const o = { tenant, run, session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
117
121
  byPeer.set(peer, o);
118
122
  (sessions.get(key) ?? sessions.set(key, new Set()).get(key)).add(o);
119
123
  if (engine.open)
120
- o.queue = o.queue.then(async () => engine.open(session, await o.context())).catch((error) => session.close(1011, String(error.message ?? error).slice(0, 120)));
124
+ o.queue = o.queue.then(() => o.run(async () => engine.open(session, await o.context()))).catch((error) => session.close(1011, String(error.message ?? error).slice(0, 120)));
121
125
  },
122
126
  message: (peer, data) => {
123
127
  const o = byPeer.get(peer);
124
128
  if (!o)
125
129
  return;
126
130
  const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
127
- o.queue = o.queue.then(async () => o.engine.message(o.session, text, await o.context())).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
131
+ o.queue = o.queue.then(() => o.run(async () => o.engine.message(o.session, text, await o.context()))).catch((error) => o.session.close(1011, String(error.message ?? error).slice(0, 120)));
128
132
  },
129
133
  close: (peer) => {
130
134
  const o = byPeer.get(peer);
@@ -140,7 +144,7 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
140
144
  // the end is written after what the session is still doing, over its own context
141
145
  // the session is gone, so a failure has no frame to go in: it is reported, never swallowed
142
146
  if (o.engine.close)
143
- o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch((error) => {
147
+ o.queue = o.queue.then(() => o.run(async () => o.engine.close(o.session, await o.context()))).catch((error) => {
144
148
  console.error(`${service}: the ${o.session.socket} socket's end was not written: ${String(error?.message ?? error)}`);
145
149
  });
146
150
  },
@@ -0,0 +1,2 @@
1
+ /** The retained World-account name grammar, shared by registration and read-only authority selection. */
2
+ export declare function isWorldAccountName(value: unknown): value is string;
@@ -0,0 +1,4 @@
1
+ /** The retained World-account name grammar, shared by registration and read-only authority selection. */
2
+ export function isWorldAccountName(value) {
3
+ return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
4
+ }
@@ -25,6 +25,9 @@
25
25
  "resources": [],
26
26
  "endpointEnvNone": "@ai-sdk/gateway calls https://ai-gateway.vercel.sh/v4/ai and an OpenAI client is given https://ai-gateway.vercel.sh/v1 in code; the World claims the host",
27
27
  "credentialDoor": {
28
+ "account": {
29
+ "field": "account"
30
+ },
28
31
  "path": "/_twin/app-credentials",
29
32
  "body": {},
30
33
  "fill": {
@@ -110,6 +113,9 @@
110
113
  "note": "the SDKs' base URL (the TypeScript and Python SDKs and Claude Code read ANTHROPIC_BASE_URL)"
111
114
  },
112
115
  "credentialDoor": {
116
+ "account": {
117
+ "field": "account"
118
+ },
113
119
  "path": "/_twin/app-credentials",
114
120
  "body": {},
115
121
  "fill": {
@@ -174,6 +180,9 @@
174
180
  "note": "The AWS SDKs read AWS_ENDPOINT_URL as every service's endpoint (https://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html)."
175
181
  },
176
182
  "credentialDoor": {
183
+ "account": {
184
+ "field": "account"
185
+ },
177
186
  "path": "/_twin/app-credentials",
178
187
  "body": {},
179
188
  "fill": {
@@ -360,6 +369,9 @@
360
369
  "credentialDoor": {
361
370
  "path": "/_twin/app-credentials",
362
371
  "body": {},
372
+ "account": {
373
+ "field": "account"
374
+ },
363
375
  "fill": {
364
376
  "CLERK_SECRET_KEY": "secret_key",
365
377
  "CLERK_API_KEY": "secret_key",
@@ -782,6 +794,9 @@
782
794
  "note": "Python e2b 2.45.0 and the JS SDK read E2B_API_URL; the JS SDK reads E2B_SANDBOX_URL and sends sandbox id/port headers; vendor hosts route through the World proxy"
783
795
  },
784
796
  "credentialDoor": {
797
+ "account": {
798
+ "field": "account"
799
+ },
785
800
  "path": "/_twin/teams",
786
801
  "body": {
787
802
  "name": "World team"
@@ -1003,6 +1018,9 @@
1003
1018
  "note": "Google GenAI SDK reads this base URL; other callers retain the routed vendor host."
1004
1019
  },
1005
1020
  "credentialDoor": {
1021
+ "account": {
1022
+ "field": "account"
1023
+ },
1006
1024
  "path": "/_twin/app-credentials",
1007
1025
  "body": {},
1008
1026
  "fill": {
@@ -1079,6 +1097,9 @@
1079
1097
  "app"
1080
1098
  ],
1081
1099
  "credentialDoor": {
1100
+ "account": {
1101
+ "field": "account"
1102
+ },
1082
1103
  "path": "/_twin/app-credentials",
1083
1104
  "body": {},
1084
1105
  "fill": {
@@ -1213,6 +1234,9 @@
1213
1234
  ],
1214
1235
  "endpointEnvNone": "Google's OAuth libraries read no base-URL variable: the endpoints are the discovery document's, claimed by the host rules.",
1215
1236
  "credentialDoor": {
1237
+ "account": {
1238
+ "field": "world_account"
1239
+ },
1216
1240
  "path": "/_twin/credentials",
1217
1241
  "body": {
1218
1242
  "client_email": "world@world-project.iam.gserviceaccount.com",
@@ -1657,12 +1681,30 @@
1657
1681
  "endpointEnv": {
1658
1682
  "name": "LIVEKIT_URL",
1659
1683
  "templates": {
1660
- "LIVEKIT_URL": "ws://${host}:${port}",
1661
- "LIVEKIT_API_KEY": "twin-livekit-key",
1662
- "LIVEKIT_API_SECRET": "twin-livekit-secret-for-the-worlds-own-server"
1684
+ "LIVEKIT_URL": "ws://${host}:${port}"
1663
1685
  },
1664
- "note": "Credentials and URL are the same ones the World starts its LiveKit server with."
1665
- }
1686
+ "note": "The native server uses a private key; its twin front accepts only keys issued for retained World accounts."
1687
+ },
1688
+ "credentialDoor": {
1689
+ "path": "/_twin/app-credentials",
1690
+ "body": {},
1691
+ "account": {
1692
+ "field": "account"
1693
+ },
1694
+ "fill": {
1695
+ "LIVEKIT_API_KEY": "api_key",
1696
+ "LIVEKIT_API_SECRET": "api_secret"
1697
+ }
1698
+ },
1699
+ "sidePorts": [
1700
+ {
1701
+ "name": "rtc-tcp",
1702
+ "option": "rtcTcpPort",
1703
+ "flag": "--rtc-tcp-port",
1704
+ "scheme": "tcp",
1705
+ "note": "ICE/TCP media advertised on loopback at this port; the vendor binds all interfaces. Forward beside signaling for a browser on another machine."
1706
+ }
1707
+ ]
1666
1708
  },
1667
1709
  "merge": {
1668
1710
  "adoption": {
@@ -1687,6 +1729,9 @@
1687
1729
  "resources": [],
1688
1730
  "endpointEnvNone": "Open Autonomy reads MODEL_GATEWAY_URL and defaults to api-gateway.merge.dev; the World claims the host",
1689
1731
  "credentialDoor": {
1732
+ "account": {
1733
+ "field": "account"
1734
+ },
1690
1735
  "path": "/_twin/organizations",
1691
1736
  "body": {},
1692
1737
  "fill": {
@@ -1725,6 +1770,9 @@
1725
1770
  "resources": [],
1726
1771
  "endpointEnvNone": "the SDKs call https://api.mistral.ai/v1 unless given a base URL in code or config; the World claims the host",
1727
1772
  "credentialDoor": {
1773
+ "account": {
1774
+ "field": "account"
1775
+ },
1728
1776
  "path": "/_twin/app-credentials",
1729
1777
  "body": {},
1730
1778
  "fill": {
@@ -1799,6 +1847,9 @@
1799
1847
  "note": "npm and bun read the registry from npm_config_registry (and .npmrc's `registry`)."
1800
1848
  },
1801
1849
  "credentialDoor": {
1850
+ "account": {
1851
+ "field": "account"
1852
+ },
1802
1853
  "path": "/_twin/app-credentials",
1803
1854
  "body": {},
1804
1855
  "fill": {
@@ -1862,6 +1913,9 @@
1862
1913
  "api_key"
1863
1914
  ],
1864
1915
  "credentialDoor": {
1916
+ "account": {
1917
+ "field": "account"
1918
+ },
1865
1919
  "path": "/_twin/app-credentials",
1866
1920
  "body": {},
1867
1921
  "fill": {
@@ -1973,6 +2027,9 @@
1973
2027
  "note": "the twin's own URL; applications reach it at <region>.connect.psdb.cloud, which the injector redirects, with the password the credentialDoor issued."
1974
2028
  },
1975
2029
  "credentialDoor": {
2030
+ "account": {
2031
+ "field": "account"
2032
+ },
1976
2033
  "path": "/_twin/app-credentials",
1977
2034
  "body": {
1978
2035
  "organization": "world",
@@ -2297,6 +2354,9 @@
2297
2354
  "received"
2298
2355
  ],
2299
2356
  "credentialDoor": {
2357
+ "account": {
2358
+ "field": "account"
2359
+ },
2300
2360
  "path": "/_twin/app-credentials",
2301
2361
  "body": {},
2302
2362
  "fill": {
@@ -2368,6 +2428,9 @@
2368
2428
  ],
2369
2429
  "endpointEnvNone": "an SDK sends to its DSN's host and sentry-cli to SENTRY_URL (sentry.io by default); the DSN the World issues names the World's ingest host, claimed by the host rules",
2370
2430
  "credentialDoor": {
2431
+ "account": {
2432
+ "field": "account"
2433
+ },
2371
2434
  "path": "/_twin/app-credentials",
2372
2435
  "body": {},
2373
2436
  "fill": {
@@ -2476,6 +2539,9 @@
2476
2539
  "app"
2477
2540
  ],
2478
2541
  "credentialDoor": {
2542
+ "account": {
2543
+ "field": "account"
2544
+ },
2479
2545
  "path": "/_twin/app-credentials",
2480
2546
  "body": {},
2481
2547
  "fill": {
@@ -2593,6 +2659,9 @@
2593
2659
  "credentialDoor": {
2594
2660
  "path": "/_twin/app-credentials",
2595
2661
  "body": {},
2662
+ "account": {
2663
+ "field": "account"
2664
+ },
2596
2665
  "fill": {
2597
2666
  "STRIPE_SECRET_KEY": "secret_key",
2598
2667
  "STRIPE_API_KEY": "secret_key",
@@ -2666,7 +2735,11 @@
2666
2735
  "managedDatabase": {
2667
2736
  "kind": "postgres",
2668
2737
  "arg": "--database",
2669
- "note": "the project's database is the World's managed postgres: the database the application's migrations and queries use, whose URL the credential door answers"
2738
+ "user": "world",
2739
+ "database": "world",
2740
+ "clock": "wall",
2741
+ "credentialField": "database_url",
2742
+ "note": "the selected Supabase twin owns its private PostgreSQL data plane; the credential door answers its URL, application migrations own application tables, and SQL uses wall time"
2670
2743
  },
2671
2744
  "serveExport": "createSupabaseTwinServer"
2672
2745
  },
@@ -2877,6 +2950,9 @@
2877
2950
  ],
2878
2951
  "endpointEnvNone": "Tinybird's clients take the API host as configuration (@chronark/zod-bird's baseUrl, tinybird-cli's --host), defaulting to https://api.tinybird.co, which the World routes by host; no SDK reads a base-URL variable of its own",
2879
2952
  "credentialDoor": {
2953
+ "account": {
2954
+ "field": "account"
2955
+ },
2880
2956
  "path": "/_twin/app-credentials",
2881
2957
  "body": {},
2882
2958
  "fill": {
@@ -3045,6 +3121,102 @@
3045
3121
  },
3046
3122
  "serveExport": "createTurbopufferTwinServer"
3047
3123
  },
3124
+ "twitch": {
3125
+ "adoption": {
3126
+ "sdks": [],
3127
+ "envStems": [
3128
+ "TWITCH"
3129
+ ]
3130
+ },
3131
+ "hosts": [
3132
+ {
3133
+ "host": "api.twitch.tv",
3134
+ "pathPattern": "^/helix/|^/_twin/"
3135
+ },
3136
+ {
3137
+ "host": "id.twitch.tv",
3138
+ "pathPattern": "^/oauth2/|^/_twin/"
3139
+ },
3140
+ {
3141
+ "host": "www.twitch.tv",
3142
+ "pathPattern": "^/$|^/login/?$|^/popout/[A-Za-z0-9_]{1,25}/chat/?$|^/[A-Za-z0-9_]{1,25}/?$|^/_twin/"
3143
+ },
3144
+ {
3145
+ "host": "twitch.tv",
3146
+ "pathPattern": "^/$|^/login/?$|^/popout/[A-Za-z0-9_]{1,25}/chat/?$|^/[A-Za-z0-9_]{1,25}/?$|^/_twin/"
3147
+ },
3148
+ {
3149
+ "host": "m.twitch.tv",
3150
+ "pathPattern": "^/$|^/login/?$|^/popout/[A-Za-z0-9_]{1,25}/chat/?$|^/[A-Za-z0-9_]{1,25}/?$|^/_twin/"
3151
+ },
3152
+ {
3153
+ "host": "dashboard.twitch.tv",
3154
+ "pathPattern": "^/u/[A-Za-z0-9_]{1,25}/(?:stream-manager|settings/stream)/?$"
3155
+ },
3156
+ {
3157
+ "host": "player.twitch.tv",
3158
+ "pathPattern": "^/$"
3159
+ },
3160
+ {
3161
+ "host": "irc-ws.chat.twitch.tv"
3162
+ },
3163
+ {
3164
+ "host": "usher.ttvnw.net",
3165
+ "pathPattern": "^/api/channel/hls/"
3166
+ }
3167
+ ],
3168
+ "archetype": "crud",
3169
+ "protocol": {
3170
+ "declared": "3",
3171
+ "major": 3,
3172
+ "standing": "current"
3173
+ },
3174
+ "transport": "rest",
3175
+ "resources": [
3176
+ "user",
3177
+ "channel",
3178
+ "stream",
3179
+ "chat_message",
3180
+ "application",
3181
+ "_token",
3182
+ "_authorization_code",
3183
+ "_client_secret",
3184
+ "_stream_key",
3185
+ "_live",
3186
+ "_live_segment"
3187
+ ],
3188
+ "endpointEnvNone": "Twitch's clients address api.twitch.tv, id.twitch.tv and irc-ws.chat.twitch.tv by their published URLs; no SDK reads a base-URL variable",
3189
+ "credentialDoor": {
3190
+ "path": "/_twin/console",
3191
+ "body": {
3192
+ "login": "worldchannel",
3193
+ "password": "world-password",
3194
+ "email": "worldchannel@example.com",
3195
+ "application": {
3196
+ "name": "World App",
3197
+ "redirect_uris": [
3198
+ "http://localhost:3000"
3199
+ ]
3200
+ }
3201
+ },
3202
+ "fill": {
3203
+ "TWITCH_CLIENT_ID": "client_id",
3204
+ "TWITCH_CLIENT_SECRET": "client_secret",
3205
+ "TWITCH_CHANNEL": "login"
3206
+ }
3207
+ },
3208
+ "sidePorts": [
3209
+ {
3210
+ "name": "ingest",
3211
+ "option": "ingestPort",
3212
+ "flag": "--ingest-port",
3213
+ "scheme": "rtmp",
3214
+ "path": "/app",
3215
+ "note": "Twitch's RTMP ingest (rtmp://<ingest-server>/app/<stream-key>): OBS publishes the channel's broadcast here"
3216
+ }
3217
+ ],
3218
+ "serveExport": "createTwitchTwinServer"
3219
+ },
3048
3220
  "upstash": {
3049
3221
  "adoption": {
3050
3222
  "sdks": [
@@ -3100,6 +3272,9 @@
3100
3272
  "note": "Redis reads its REST URL; QStash and Workflow read QSTASH_URL. Direct SDK transports use the twin endpoint and issued token; other clients also use host routing."
3101
3273
  },
3102
3274
  "credentialDoor": {
3275
+ "account": {
3276
+ "field": "account"
3277
+ },
3103
3278
  "path": "/_twin/app-credentials",
3104
3279
  "body": {
3105
3280
  "owner": "owner@world.test"
@@ -3165,6 +3340,9 @@
3165
3340
  ],
3166
3341
  "endpointEnvNone": "the applications reach api.vercel.com and the Edge Config connection string's host, which the World routes to the twin",
3167
3342
  "credentialDoor": {
3343
+ "account": {
3344
+ "field": "account"
3345
+ },
3168
3346
  "path": "/_twin/app-credentials",
3169
3347
  "body": {
3170
3348
  "team": "world",
@@ -3403,6 +3581,9 @@
3403
3581
  "upload"
3404
3582
  ],
3405
3583
  "credentialDoor": {
3584
+ "account": {
3585
+ "field": "account"
3586
+ },
3406
3587
  "path": "/_twin/app-credentials",
3407
3588
  "body": {},
3408
3589
  "fill": {
@@ -3441,6 +3622,9 @@
3441
3622
  "resources": [],
3442
3623
  "endpointEnvNone": "the SDK calls https://api.x.ai/v1 unless given a base URL in code; the World claims the host",
3443
3624
  "credentialDoor": {
3625
+ "account": {
3626
+ "field": "account"
3627
+ },
3444
3628
  "path": "/_twin/app-credentials",
3445
3629
  "body": {},
3446
3630
  "fill": {
package/inject.cjs CHANGED
@@ -358,8 +358,8 @@ function strictEgressEnabled() {
358
358
  }
359
359
 
360
360
  // THE WORLD'S DECISION (docs/contributing/architecture.md, "Capability decisions"): every seam below asks
361
- // capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one; a
362
- // local light guest, whose HTTP leaves through its guest listener; or a process of an image run, which carries its
361
+ // capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one; or
362
+ // a process of an image run, which carries its
363
363
  // image's declaration (VOLTER_WORLD_DECLARATION) and is confined from the host's network. What the World grants is its
364
364
  // published policy. The seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
365
365
  function worldGrant() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.160",
3
+ "version": "3.0.162",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -182,5 +182,13 @@
182
182
  },
183
183
  "optionalDependencies": {
184
184
  "@xmldom/xmldom": "0.9.12"
185
+ },
186
+ "packageManager": "pnpm@12.15.0",
187
+ "devEngines": {
188
+ "packageManager": {
189
+ "name": "pnpm",
190
+ "version": "12.15.0",
191
+ "onFail": "error"
192
+ }
185
193
  }
186
194
  }