@undefineds.co/xpod 0.4.28 → 0.4.30

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 (179) hide show
  1. package/dist/api/ApiServer.js +4 -0
  2. package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
  3. package/dist/api/ai-gateway/AiGatewayService.js +145 -6
  4. package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
  5. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
  6. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
  7. package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
  8. package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
  9. package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
  10. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
  11. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
  12. package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
  13. package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
  14. package/dist/api/ai-gateway/connect/index.d.ts +40 -0
  15. package/dist/api/ai-gateway/connect/index.js +194 -4
  16. package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
  17. package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
  18. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
  19. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
  20. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
  21. package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
  22. package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
  23. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
  24. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
  25. package/dist/api/auth/AuthContext.d.ts +4 -0
  26. package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
  27. package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
  28. package/dist/api/auth/SolidSessionFactory.js +114 -4
  29. package/dist/api/chatkit/pod-store.d.ts +8 -1
  30. package/dist/api/chatkit/pod-store.js +54 -3
  31. package/dist/api/container/common.js +34 -60
  32. package/dist/api/container/index.js +0 -3
  33. package/dist/api/container/routes.js +1 -8
  34. package/dist/api/container/types.d.ts +2 -9
  35. package/dist/api/container/types.js +0 -5
  36. package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
  37. package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
  38. package/dist/api/middleware/AuthMiddleware.js +29 -1
  39. package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
  40. package/dist/cli/index.js +2 -1
  41. package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
  42. package/dist/runtime/ingress-port.d.ts +9 -2
  43. package/dist/runtime/ingress-port.js +7 -2
  44. package/dist/runtime/local-secret-file.d.ts +1 -1
  45. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
  46. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
  48. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
  49. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
  50. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
  51. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
  52. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  53. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
  54. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
  55. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
  56. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
  57. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
  58. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
  59. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
  60. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
  61. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
  62. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
  63. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
  64. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
  65. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
  66. package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
  67. package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
  68. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
  69. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
  70. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
  71. package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
  72. package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
  73. package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
  74. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
  75. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  76. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
  77. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
  78. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
  79. package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
  80. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
  81. package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
  82. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  83. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  84. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  85. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  86. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  87. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  88. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  89. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  90. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
  91. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  92. package/package.json +2 -2
  93. package/static/app/assets/main.css +1 -1
  94. package/static/app/assets/main.js +18 -18
  95. package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
  96. package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
  97. package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
  98. package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
  99. package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
  100. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
  101. package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
  102. package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
  103. package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
  104. package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
  105. package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
  106. package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
  107. package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
  108. package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
  109. package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
  110. package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
  111. package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
  112. package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
  113. package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
  114. package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
  115. package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
  116. package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
  117. package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
  118. package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
  119. package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
  120. package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
  121. package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
  122. package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
  123. package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
  124. package/static/dashboard/dashboard.html +2 -2
  125. package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
  126. package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
  127. package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
  128. package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
  129. package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
  130. package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
  131. package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
  132. package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
  133. package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
  134. package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
  135. package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
  136. package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
  137. package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
  138. package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
  139. package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
  140. package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
  141. package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
  142. package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
  143. package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
  144. package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
  145. package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
  146. package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
  147. package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
  148. package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
  149. package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
  150. package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
  151. package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
  152. package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
  153. package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
  154. package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
  155. package/static/settings/auth-callback.html +3 -3
  156. package/static/settings/settings.html +3 -3
  157. package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
  158. package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
  159. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
  160. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
  161. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
  162. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
  163. package/dist/runtime/gateway-locator-secret.d.ts +0 -5
  164. package/dist/runtime/gateway-locator-secret.js +0 -30
  165. package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
  166. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
  167. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
  168. package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
  169. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
  170. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
  171. package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
  172. package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
  173. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
  174. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
  175. package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
  176. package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
  177. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
  178. package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
  179. package/static/settings/assets/pod-collections-host-DjK2xf-b.js +0 -8
@@ -1,4 +1,4 @@
1
- import { universalAccess } from '@inrupt/solid-client';
1
+ import { addUrl, createSolidDataset, createThing, getLinkedResourceUrlAll, getResourceInfo, getSolidDataset, getThingAll, getUrlAll, saveSolidDatasetAt, setThing, universalAccess, } from '@inrupt/solid-client';
2
2
  /**
3
3
  * Creates the host-owned permission broker used by trusted applets. The applet
4
4
  * declares exact Pod resources; the host is the only layer allowed to inspect
@@ -6,16 +6,38 @@ import { universalAccess } from '@inrupt/solid-client';
6
6
  */
7
7
  export function createSolidPermissionCapability(options) {
8
8
  const access = options.access ?? universalAccess;
9
+ // Only this capability's direct-mode changes can be undone. A newly mounted
10
+ // capability must never treat unrelated existing permissions as its own grant.
11
+ const grants = new Map();
12
+ const keyFor = (request, url) => JSON.stringify([request.appletId, request.service.webId, url]);
9
13
  return {
10
14
  inspectAgentAccess: (request) => inspect(request, options.fetch, access),
11
15
  ensureAgentAccess: async (request) => {
12
16
  try {
13
17
  for (const resource of request.resources) {
14
18
  await ensureResource(resource.url, resource.mediaType, options.fetch);
19
+ await ensureDeclaredAcr(resource.url, options.fetch);
20
+ const current = agentModes(await access.getAgentAccess(resource.url, request.service.webId, { fetch: options.fetch }));
21
+ const key = keyFor(request, resource.url);
22
+ const previous = grants.get(key);
23
+ if (previous && !sameModes(current, previous.after))
24
+ throw new Error('Agent access changed outside this capability.');
25
+ if (hasRequestedAccess(current, resource.access)) {
26
+ if (!previous)
27
+ grants.set(key, { before: current, after: current, changed: false });
28
+ continue;
29
+ }
30
+ // Add only requested modes. Existing owner control and unrequested
31
+ // access are not removed as a side effect of granting an applet.
15
32
  const granted = await access.setAgentAccess(resource.url, request.service.webId, toAccessModes(resource.access), { fetch: options.fetch });
16
33
  if (!granted || !hasRequestedAccess(granted, resource.access)) {
17
34
  return status('permissionDenied', request, 'Pod did not grant the requested service access.');
18
35
  }
36
+ grants.set(key, {
37
+ before: previous?.before ?? current,
38
+ after: agentModes({ ...current, ...granted }),
39
+ changed: true,
40
+ });
19
41
  }
20
42
  return status('granted', request);
21
43
  }
@@ -25,13 +47,34 @@ export function createSolidPermissionCapability(options) {
25
47
  },
26
48
  revokeAgentAccess: async (request) => {
27
49
  try {
50
+ // Validate every attribution before changing any resource. A stale or
51
+ // cold capability cannot silently undo another operation's permissions.
52
+ const pending = [];
28
53
  for (const resource of request.resources) {
29
- const revoked = await access.setAgentAccess(resource.url, request.service.webId, { read: false, append: false, write: false, controlRead: false, controlWrite: false }, { fetch: options.fetch });
30
- if (!revoked) {
31
- return status('permissionDenied', request, 'Pod did not revoke the service access.');
54
+ const grant = grants.get(keyFor(request, resource.url));
55
+ if (!grant)
56
+ throw new Error('No grant attribution exists in this capability; existing access was not revoked.');
57
+ const current = agentModes(await access.getAgentAccess(resource.url, request.service.webId, { fetch: options.fetch }));
58
+ if (!sameModes(current, grant.after))
59
+ throw new Error('Agent access changed outside this capability; access was not revoked.');
60
+ pending.push({ resource, grant });
61
+ }
62
+ for (const { resource, grant } of pending) {
63
+ if (!grant.changed)
64
+ continue;
65
+ const restore = {};
66
+ for (const mode of AGENT_MODES)
67
+ if (grant.before[mode] !== grant.after[mode])
68
+ restore[mode] = grant.before[mode];
69
+ const restored = await access.setAgentAccess(resource.url, request.service.webId, restore, { fetch: options.fetch });
70
+ if (!restored || !sameModes(agentModes({ ...grant.after, ...restored }), grant.before)) {
71
+ return status('permissionDenied', request, 'Pod did not restore this capability grant.');
32
72
  }
73
+ grant.after = grant.before;
74
+ grant.changed = false;
33
75
  }
34
- return status('missing', request);
76
+ const result = await inspect(request, options.fetch, access);
77
+ return result.status === 'granted' ? { ...result, message: 'Existing agent access was retained.' } : result;
35
78
  }
36
79
  catch (error) {
37
80
  return status('permissionDenied', request, errorMessage(error));
@@ -39,6 +82,15 @@ export function createSolidPermissionCapability(options) {
39
82
  },
40
83
  };
41
84
  }
85
+ const AGENT_MODES = ['read', 'append', 'write', 'controlRead', 'controlWrite'];
86
+ function agentModes(value) {
87
+ if (!value || AGENT_MODES.some(mode => typeof value[mode] !== 'boolean'))
88
+ throw new Error('Unable to inspect direct agent access.');
89
+ return Object.fromEntries(AGENT_MODES.map(mode => [mode, value[mode]]));
90
+ }
91
+ function sameModes(left, right) {
92
+ return AGENT_MODES.every(mode => left[mode] === right[mode]);
93
+ }
42
94
  async function inspect(request, fetch, access) {
43
95
  try {
44
96
  for (const resource of request.resources) {
@@ -63,13 +115,81 @@ async function ensureResource(url, mediaType, fetch) {
63
115
  await ensureParentContainers(url, fetch);
64
116
  const created = await fetch(url, {
65
117
  method: 'PUT',
66
- headers: { 'content-type': mediaType },
67
- body: '# Created by the Xpod applet permission broker.\n',
118
+ headers: { 'content-type': mediaType, 'if-none-match': '*' },
119
+ body: mediaType === 'application/json' ? '{}\n' : '# Created by the Xpod applet permission broker.\n',
68
120
  });
69
- if (!created.ok) {
121
+ if (!created.ok && created.status !== 412) {
70
122
  throw new Error(`Unable to create Pod resource (${created.status}).`);
71
123
  }
72
124
  }
125
+ const ACP = 'http://www.w3.org/ns/solid/acp#';
126
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
127
+ /** Bootstrap only a server-declared, missing target ACR. Parent ACP policies
128
+ * remain effective; no owner or inherited grants are copied or replaced. */
129
+ async function ensureDeclaredAcr(resourceUrl, fetch) {
130
+ const info = await getResourceInfo(resourceUrl, { fetch });
131
+ const links = getLinkedResourceUrlAll(info).acl;
132
+ if (!links || links.length !== 1)
133
+ throw new Error('Missing unambiguous access-control Link.');
134
+ const linked = new URL(links[0]);
135
+ const target = new URL(resourceUrl);
136
+ if (linked.origin !== target.origin || linked.username || linked.password || linked.search || linked.hash) {
137
+ throw new Error('Access-control Link is outside the resource authority.');
138
+ }
139
+ const response = await fetch(linked.href, { method: 'HEAD' });
140
+ const declaresAcp = declaresAcpType(response.headers.get('link'));
141
+ await response.arrayBuffer();
142
+ if (response.ok) {
143
+ if (declaresAcp)
144
+ await assertTargetAcr(linked.href, resourceUrl, fetch);
145
+ return;
146
+ }
147
+ if (response.status !== 404)
148
+ throw new Error(`Unable to inspect access-control resource (${response.status}).`);
149
+ // WAC initialization remains the responsibility of the ecosystem API.
150
+ if (!declaresAcp)
151
+ return;
152
+ const thing = addUrl(addUrl(createThing({ url: linked.href }), RDF_TYPE, `${ACP}AccessControlResource`), `${ACP}resource`, resourceUrl);
153
+ const dataset = setThing(createSolidDataset(), thing);
154
+ let creationStatus;
155
+ const createOnlyFetch = async (input, init) => {
156
+ const inputUrl = input instanceof Request ? input.url : String(input);
157
+ if (inputUrl !== linked.href || init?.method !== 'PUT')
158
+ throw new Error('Unexpected access-control creation request.');
159
+ const headers = new Headers(init.headers);
160
+ headers.set('If-None-Match', '*');
161
+ const created = await fetch(input, { ...init, headers });
162
+ creationStatus = created.status;
163
+ return created;
164
+ };
165
+ try {
166
+ await saveSolidDatasetAt(linked.href, dataset, { fetch: createOnlyFetch });
167
+ }
168
+ catch (error) {
169
+ if (creationStatus !== 412)
170
+ throw error;
171
+ // A competing writer won. Read its real policies; never replace them.
172
+ await assertTargetAcr(linked.href, resourceUrl, fetch);
173
+ }
174
+ }
175
+ function declaresAcpType(header) {
176
+ return (header ?? '').split(/,(?=\s*<)/u).some((link) => {
177
+ const match = /^\s*<([^<>]+)>\s*;(.*)$/u.exec(link);
178
+ if (match?.[1] !== `${ACP}AccessControlResource`)
179
+ return false;
180
+ const rel = /(?:^|;)\s*rel\s*=\s*(?:"([^"]*)"|([^;\s]+))(?:\s*;|\s*$)/u.exec(match[2]);
181
+ return (rel?.[1] ?? rel?.[2] ?? '').split(/\s+/u).includes('type');
182
+ });
183
+ }
184
+ async function assertTargetAcr(acrUrl, resourceUrl, fetch) {
185
+ const dataset = await getSolidDataset(acrUrl, { fetch });
186
+ const controls = getThingAll(dataset).filter((thing) => getUrlAll(thing, RDF_TYPE).includes(`${ACP}AccessControlResource`));
187
+ if (controls.length !== 1)
188
+ throw new Error('Invalid target access-control resource.');
189
+ const resources = getUrlAll(controls[0], `${ACP}resource`);
190
+ if (resources.length !== 1 || resources[0] !== resourceUrl)
191
+ throw new Error('Access-control resource targets another resource.');
192
+ }
73
193
  async function ensureParentContainers(resourceUrl, fetch) {
74
194
  const resource = new URL(resourceUrl);
75
195
  const missing = [];
@@ -100,11 +220,9 @@ async function ensureParentContainers(resourceUrl, fetch) {
100
220
  }
101
221
  function toAccessModes(access) {
102
222
  return {
103
- read: access.read === true,
104
- append: access.append === true,
105
- write: access.write === true,
106
- controlRead: false,
107
- controlWrite: false,
223
+ ...(access.read ? { read: true } : {}),
224
+ ...(access.append ? { append: true } : {}),
225
+ ...(access.write ? { write: true } : {}),
108
226
  };
109
227
  }
110
228
  function hasRequestedAccess(actual, requested) {
@@ -175,7 +175,7 @@ export interface SolidAgentAccess {
175
175
  export interface SolidServiceAccessResource {
176
176
  id: string;
177
177
  url: string;
178
- mediaType: 'text/turtle';
178
+ mediaType: 'text/turtle' | 'application/json';
179
179
  access: SolidAgentAccess;
180
180
  }
181
181
  export interface SolidServiceAccessRequest {
@@ -343,11 +343,17 @@ export interface AiClientCredentialSummary {
343
343
  resource: string;
344
344
  }
345
345
  export interface AiClientCredentialsCapability {
346
+ /**
347
+ * Issues one Account client credential. `apiKey` is the ephemeral
348
+ * `sk-<base64(client_id:client_secret)>` wrapper and is only ever returned
349
+ * here; `clientId` is the Account identity the credential is revoked by.
350
+ */
346
351
  create(input: {
347
352
  name: string;
348
353
  webId: string;
349
354
  }): Promise<{
350
355
  apiKey: string;
356
+ clientId: string;
351
357
  resource: string;
352
358
  }>;
353
359
  /** Credentials the Account still knows about; the secret is never returned. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undefineds.co/extension-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -217,6 +217,36 @@ function rowBelongsToDocument(descriptor, documentUrl, podUrl, row) {
217
217
  // src/mapping.ts
218
218
  var XSD = "http://www.w3.org/2001/XMLSchema#";
219
219
  var ABSOLUTE_URI = /^[a-z][a-z0-9+.-]*:/iu;
220
+ function createProjectionNormalizer({ descriptor, table, database }) {
221
+ if (typeof database.getDialect !== "function" || typeof table.getColumns !== "function")
222
+ return (row) => row;
223
+ const resolver = database.getDialect().getUriResolver();
224
+ const columns = table.getColumns();
225
+ const bindings = [...fieldBindings(descriptor, table).values()].filter((binding) => binding.descriptor.type === "uri" && !binding.descriptor.secret && binding.column !== undefined);
226
+ const tableNameRegistry = new Map;
227
+ const tableRegistry = new Map;
228
+ for (const resource of Object.values(database.getSchema() ?? {})) {
229
+ if (!resource || typeof resource.getType !== "function")
230
+ continue;
231
+ tableNameRegistry.set(resource.config.name, resource);
232
+ const type = resource.getType();
233
+ tableRegistry.set(type, [...tableRegistry.get(type) ?? [], resource]);
234
+ }
235
+ return (row) => {
236
+ const source = row;
237
+ const normalized = { ...source };
238
+ const record = descriptorRowToColumnValues(descriptor, table, source, { resourceId: String(source.id ?? "") });
239
+ for (const binding of bindings) {
240
+ const column = columns[binding.column];
241
+ if (!column)
242
+ continue;
243
+ const resolve = (value) => typeof value === "string" ? resolver.resolveLink(value, column, { currentTable: table, record, tableNameRegistry, tableRegistry }) : value;
244
+ const value = source[binding.field];
245
+ normalized[binding.field] = binding.descriptor.array && Array.isArray(value) ? value.map(resolve) : resolve(value);
246
+ }
247
+ return normalized;
248
+ };
249
+ }
220
250
  function tableColumnPredicates(table) {
221
251
  const mapping = table.getMapping();
222
252
  const predicates = new Map;
@@ -622,7 +652,8 @@ function markConflict(pending, entry, server) {
622
652
  });
623
653
  pending.settle(entry, new PodCollectionError("write_conflict", `row ${entry.key} was changed by someone else; server wins`));
624
654
  }
625
- function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_WRITE_ONLY) {
655
+ function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_WRITE_ONLY, projectionContext) {
656
+ const normalize = projectionContext ? createProjectionNormalizer(projectionContext) : (row) => row;
626
657
  for (const entry of pending.entries.values()) {
627
658
  if (entry.settled)
628
659
  continue;
@@ -637,7 +668,7 @@ function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_WRITE_O
637
668
  }
638
669
  continue;
639
670
  }
640
- if (projectionCovers(server, entry.intent, writeOnly)) {
671
+ if (projectionCovers(server && normalize(server), normalize(entry.intent), writeOnly)) {
641
672
  pending.settle(entry);
642
673
  continue;
643
674
  }
@@ -684,7 +715,8 @@ async function writeField(context, fieldName, options) {
684
715
  function createMutationHandlers(context) {
685
716
  const { descriptor, table, pending } = context;
686
717
  const fieldOrder = projectionFieldOrder(descriptor);
687
- const hashOf = (row) => projectionHash(row, fieldOrder);
718
+ const normalize = createProjectionNormalizer(context);
719
+ const hashOf = (row) => projectionHash(normalize(row), fieldOrder);
688
720
  const uriArrayFieldNames = new Set(Object.entries(descriptor.fields).filter(([, field]) => isUriArrayField(field)).map(([name]) => name));
689
721
  const columnBindings = fieldBindings(descriptor, table);
690
722
  const columnOf = (field) => columnBindings.get(field)?.column;
@@ -844,7 +876,8 @@ function createPodSync(options) {
844
876
  const confirmRetries = options.confirmRetries ?? DEFAULT_CONFIRM_RETRIES;
845
877
  const fieldOrder = projectionFieldOrder(descriptor);
846
878
  const writeOnly = writeOnlyFields(descriptor);
847
- const hashOf = (row) => projectionHash(row, fieldOrder);
879
+ const normalize = createProjectionNormalizer(options);
880
+ const hashOf = (row) => projectionHash(normalize(row), fieldOrder);
848
881
  let params;
849
882
  let subscription;
850
883
  let feedAvailable = false;
@@ -955,7 +988,7 @@ function createPodSync(options) {
955
988
  failureReported = false;
956
989
  sync.markReady();
957
990
  }
958
- reconcilePendingWrites(pending, next, hashOf, writeOnly);
991
+ reconcilePendingWrites(pending, next, hashOf, writeOnly, options);
959
992
  syncState = feedAvailable ? "live" : "unavailable";
960
993
  }
961
994
  async function confirmPending(key) {
@@ -1,6 +1,14 @@
1
1
  import type { AnyPodTable, SolidDatabase } from '@undefineds.co/drizzle-solid';
2
2
  import type { PodModelDescriptor, PodModelFieldDescriptor } from '@undefineds.co/models';
3
3
  import type { PodSubjectRow, RowOf } from './types.js';
4
+ export interface PodProjectionContext {
5
+ descriptor: PodModelDescriptor;
6
+ table: AnyPodTable;
7
+ database: SolidDatabase;
8
+ }
9
+ /** Compare mapped URI fields in the same space the bound ORM writes them.
10
+ * This does not change the collection row or introduce an address resolver. */
11
+ export declare function createProjectionNormalizer({ descriptor, table, database }: PodProjectionContext): <R extends object>(row: R) => R;
4
12
  /** RDF term(只覆盖本层会产生的两种)。 */
5
13
  export interface PodRdfTerm {
6
14
  termType: 'NamedNode' | 'Literal';
@@ -20,6 +20,40 @@ import { rowKeyOf, rowKeyVariable } from './layout.js';
20
20
  */
21
21
  const XSD = 'http://www.w3.org/2001/XMLSchema#';
22
22
  const ABSOLUTE_URI = /^[a-z][a-z0-9+.-]*:/iu;
23
+ /** Compare mapped URI fields in the same space the bound ORM writes them.
24
+ * This does not change the collection row or introduce an address resolver. */
25
+ export function createProjectionNormalizer({ descriptor, table, database }) {
26
+ if (typeof database.getDialect !== 'function' || typeof table.getColumns !== 'function')
27
+ return row => row;
28
+ const resolver = database.getDialect().getUriResolver();
29
+ const columns = table.getColumns();
30
+ const bindings = [...fieldBindings(descriptor, table).values()]
31
+ .filter(binding => binding.descriptor.type === 'uri' && !binding.descriptor.secret && binding.column !== undefined);
32
+ const tableNameRegistry = new Map();
33
+ const tableRegistry = new Map();
34
+ for (const resource of Object.values((database.getSchema() ?? {}))) {
35
+ if (!resource || typeof resource.getType !== 'function')
36
+ continue;
37
+ tableNameRegistry.set(resource.config.name, resource);
38
+ const type = resource.getType();
39
+ tableRegistry.set(type, [...(tableRegistry.get(type) ?? []), resource]);
40
+ }
41
+ return (row) => {
42
+ const source = row;
43
+ const normalized = { ...source };
44
+ const record = descriptorRowToColumnValues(descriptor, table, source, { resourceId: String(source.id ?? '') });
45
+ for (const binding of bindings) {
46
+ const column = columns[binding.column];
47
+ if (!column)
48
+ continue;
49
+ const resolve = (value) => typeof value === 'string'
50
+ ? resolver.resolveLink(value, column, { currentTable: table, record, tableNameRegistry, tableRegistry }) : value;
51
+ const value = source[binding.field];
52
+ normalized[binding.field] = binding.descriptor.array && Array.isArray(value) ? value.map(resolve) : resolve(value);
53
+ }
54
+ return normalized;
55
+ };
56
+ }
23
57
  /** 表列 → 谓词(跳过 `@id` 虚列)。 */
24
58
  export function tableColumnPredicates(table) {
25
59
  const mapping = table.getMapping();
@@ -2,6 +2,7 @@ import type { AnyPodTable, SolidDatabase } from '@undefineds.co/drizzle-solid';
2
2
  import type { PodModelDescriptor } from '@undefineds.co/models';
3
3
  import type { InsertMutationFn, UpdateMutationFn, DeleteMutationFn } from '@tanstack/db';
4
4
  import type { PodRowConflict, RowOf } from './types.js';
5
+ import { type PodProjectionContext } from './mapping.js';
5
6
  /**
6
7
  * 未确认的本地写。`sync` 每轮读回文档后调用 `reconcilePendingWrites()`:
7
8
  * 读回意图 → 确认;读回第三方值 → 冲突(server wins + 标记);仍未见意图 → 继续等。
@@ -53,7 +54,7 @@ export declare function markConflict<R extends object>(pending: PodPendingWrites
53
54
  * `diff.projectionCovers()`。可读字段的比对一字未松:意图里任何一个非 secret 字段
54
55
  * 在服务端行里对不上,仍然走冲突分支。
55
56
  */
56
- export declare function reconcilePendingWrites<R extends object>(pending: PodPendingWrites<R>, next: ReadonlyMap<string, R>, hashOf: (row: R) => string, writeOnly?: ReadonlySet<string>): void;
57
+ export declare function reconcilePendingWrites<R extends object>(pending: PodPendingWrites<R>, next: ReadonlyMap<string, R>, hashOf: (row: R) => string, writeOnly?: ReadonlySet<string>, projectionContext?: PodProjectionContext): void;
57
58
  export interface PodMutationContext<D extends PodModelDescriptor> {
58
59
  descriptor: D;
59
60
  table: AnyPodTable;
@@ -1,5 +1,5 @@
1
1
  import { PodCollectionError } from './types.js';
2
- import { descriptorRowToColumnValues, fieldBindings, isUriArrayField, normalizeMutationRow, normalizeUriArray, projectionFieldOrder, } from './mapping.js';
2
+ import { descriptorRowToColumnValues, createProjectionNormalizer, fieldBindings, isUriArrayField, normalizeMutationRow, normalizeUriArray, projectionFieldOrder, } from './mapping.js';
3
3
  import { documentOfIri, resourceIdForRow, subjectIriForRow } from './layout.js';
4
4
  import { sessionFetchOf } from './read.js';
5
5
  import { projectionCovers, projectionHash } from './diff.js';
@@ -75,7 +75,8 @@ export function markConflict(pending, entry, server) {
75
75
  * `diff.projectionCovers()`。可读字段的比对一字未松:意图里任何一个非 secret 字段
76
76
  * 在服务端行里对不上,仍然走冲突分支。
77
77
  */
78
- export function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_WRITE_ONLY) {
78
+ export function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_WRITE_ONLY, projectionContext) {
79
+ const normalize = projectionContext ? createProjectionNormalizer(projectionContext) : (row) => row;
79
80
  for (const entry of pending.entries.values()) {
80
81
  if (entry.settled)
81
82
  continue;
@@ -91,7 +92,7 @@ export function reconcilePendingWrites(pending, next, hashOf, writeOnly = EMPTY_
91
92
  }
92
93
  continue;
93
94
  }
94
- if (projectionCovers(server, entry.intent, writeOnly)) {
95
+ if (projectionCovers(server && normalize(server), normalize(entry.intent), writeOnly)) {
95
96
  // 意图的可读字段都在服务端行里(插入意图可能只带一部分字段;带 secret 的意图
96
97
  // 只按可读部分确认,secret 的证据是写调用已经成功)。
97
98
  pending.settle(entry);
@@ -146,7 +147,8 @@ export async function writeField(context, fieldName, options) {
146
147
  export function createMutationHandlers(context) {
147
148
  const { descriptor, table, pending } = context;
148
149
  const fieldOrder = projectionFieldOrder(descriptor);
149
- const hashOf = (row) => projectionHash(row, fieldOrder);
150
+ const normalize = createProjectionNormalizer(context);
151
+ const hashOf = (row) => projectionHash(normalize(row), fieldOrder);
150
152
  const uriArrayFieldNames = new Set(Object.entries(descriptor.fields)
151
153
  .filter(([, field]) => isUriArrayField(field))
152
154
  .map(([name]) => name));
@@ -1,5 +1,5 @@
1
1
  import { conditionalDocumentRead } from './read.js';
2
- import { mapSubjectRows, projectionFieldOrder, writeOnlyFields } from './mapping.js';
2
+ import { createProjectionNormalizer, mapSubjectRows, projectionFieldOrder, writeOnlyFields } from './mapping.js';
3
3
  import { computeDocumentDiff, projectionHash, stripVirtualProps } from './diff.js';
4
4
  import { subscribeDocumentFeed } from './feed.js';
5
5
  import { reconcilePendingWrites } from './mutations.js';
@@ -50,7 +50,8 @@ export function createPodSync(options) {
50
50
  const fieldOrder = projectionFieldOrder(descriptor);
51
51
  // `secret: true` 的字段读不回来,确认协议要按这条规则把它们排除在比对之外(§4.2、§9-7)。
52
52
  const writeOnly = writeOnlyFields(descriptor);
53
- const hashOf = (row) => projectionHash(row, fieldOrder);
53
+ const normalize = createProjectionNormalizer(options);
54
+ const hashOf = (row) => projectionHash(normalize(row), fieldOrder);
54
55
  let params;
55
56
  let subscription;
56
57
  let feedAvailable = false;
@@ -182,7 +183,7 @@ export function createPodSync(options) {
182
183
  failureReported = false;
183
184
  sync.markReady();
184
185
  }
185
- reconcilePendingWrites(pending, next, hashOf, writeOnly);
186
+ reconcilePendingWrites(pending, next, hashOf, writeOnly, options);
186
187
  syncState = feedAvailable ? 'live' : 'unavailable';
187
188
  }
188
189
  async function confirmPending(key) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undefineds.co/pod-collections",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,5 +11,5 @@ export interface IdpChromeProps {
11
11
  serviceLabel?: string;
12
12
  locale?: PodSignInLocale;
13
13
  }
14
- /** 44px tinted bar at the top of every sign-in-service page (B group). */
14
+ /** Service identity stays readable above the form, even on a narrow host. */
15
15
  export declare function IdpChrome({ serviceName, serviceHost, icon, serviceLabel, locale }: IdpChromeProps): import("react").JSX.Element;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { resolvePodSignInCopy } from './copy.js';
3
3
  import { XpodMark } from './XpodMark.js';
4
- /** 44px tinted bar at the top of every sign-in-service page (B group). */
4
+ /** Service identity stays readable above the form, even on a narrow host. */
5
5
  export function IdpChrome({ serviceName, serviceHost, icon, serviceLabel, locale }) {
6
6
  const label = serviceLabel ?? resolvePodSignInCopy(locale).serviceLabel;
7
- return (_jsxs("div", { "data-pod-sign-in": "idp-chrome", className: "flex h-11 shrink-0 items-center gap-2 border-b border-border bg-[hsl(var(--sunken))] px-4 text-[13px] text-foreground", children: [_jsx("span", { className: "flex shrink-0 items-center justify-center", children: icon ?? _jsx(XpodMark, { size: 24 }) }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [serviceName, " \u00B7 ", label] }), serviceHost ? _jsx("span", { className: "ml-auto truncate font-mono text-xs text-muted-foreground", children: serviceHost }) : null] }));
7
+ return (_jsxs("div", { "data-pod-sign-in": "idp-chrome", className: "flex min-h-14 shrink-0 items-center gap-3 border-b border-border bg-[hsl(var(--sunken))] px-6 py-2 text-[13px] text-foreground min-[400px]:px-8", children: [_jsx("span", { className: "flex shrink-0 items-center justify-center", children: icon ?? _jsx(XpodMark, { size: 24 }) }), _jsxs("span", { className: "min-w-0 max-w-[50%] shrink-0 truncate font-medium", children: [serviceName, " \u00B7 ", label] }), serviceHost ? _jsx("span", { className: "ml-auto truncate font-mono text-xs text-muted-foreground", children: serviceHost }) : null] }));
8
8
  }
@@ -31,7 +31,7 @@ export function IdpSignInView(props) {
31
31
  if (!pending)
32
32
  onSubmit({ email: email.trim(), password });
33
33
  };
34
- return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-sign-in", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.signIn }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onRegister || registerHref ? (_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0", children: [_jsx("span", { className: "whitespace-nowrap text-xs text-muted-foreground", children: copy.noAccount }), _jsx(TextAction, { href: registerHref, disabled: pending, onClick: onRegister, children: copy.registerLink })] })) : _jsx("span", {}), onUseOtherSolid ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'text-primary hover:underline'), disabled: pending, onClick: onUseOtherSolid, children: copy.useOtherSolid })) : null] })] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.signInTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { "data-pod-sign-in": "fields", className: "flex flex-col gap-4", children: [_jsx(Field, { label: copy.email, error: fieldErrors?.email, children: (fieldProps) => (_jsx(EmailInput, { ...fieldProps, name: "email", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: email, disabled: pending, onChange: (event) => {
34
+ return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-sign-in", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.signIn }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onRegister || registerHref ? (_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0", children: [_jsx("span", { className: "whitespace-nowrap text-xs text-muted-foreground", children: copy.noAccount }), _jsx(TextAction, { href: registerHref, disabled: pending, onClick: onRegister, children: copy.registerLink })] })) : _jsx("span", {}), onUseOtherSolid ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'text-primary hover:underline'), disabled: pending, onClick: onUseOtherSolid, children: copy.useOtherSolid })) : null] })] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.signInTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { "data-pod-sign-in": "fields", className: "flex flex-col gap-5", children: [_jsx(Field, { label: copy.email, error: fieldErrors?.email, children: (fieldProps) => (_jsx(EmailInput, { ...fieldProps, name: "email", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: email, disabled: pending, onChange: (event) => {
35
35
  setEmail(event.target.value);
36
36
  onFieldChange?.('email', event.target.value);
37
37
  } })) }), _jsx(Field, { label: copy.password, error: fieldErrors?.password, labelAside: onForgot || forgotHref ? (_jsx(TextAction, { className: "min-h-7 text-[13px]", href: forgotHref, onClick: onForgot, children: copy.forgotPassword })) : undefined, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "password", type: "password", autoComplete: "current-password", className: inputClass, value: password, disabled: pending, onChange: (event) => {
@@ -56,7 +56,7 @@ export function IdpRegisterView(props) {
56
56
  password,
57
57
  });
58
58
  };
59
- return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-register", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.registerSubmit }), onSignIn ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'self-center text-primary hover:underline'), disabled: pending, onClick: onSignIn, children: copy.haveAccount })) : null] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.registerTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { className: "flex flex-col gap-4", children: [requireUsername ? (_jsx(Field, { label: copy.username, error: fieldErrors?.username, hint: usernamePreview ? _jsx("span", { role: "status", className: "font-mono text-xs", children: usernamePreview }) : undefined, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "username", type: "text", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: username, disabled: pending, onChange: (event) => {
59
+ return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-register", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.registerSubmit }), onSignIn ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'self-center text-primary hover:underline'), disabled: pending, onClick: onSignIn, children: copy.haveAccount })) : null] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.registerTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { className: "flex flex-col gap-5", children: [requireUsername ? (_jsx(Field, { label: copy.username, error: fieldErrors?.username, hint: usernamePreview ? _jsx("span", { role: "status", className: "font-mono text-xs", children: usernamePreview }) : undefined, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "username", type: "text", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: username, disabled: pending, onChange: (event) => {
60
60
  setUsername(event.target.value);
61
61
  onFieldChange?.('username', event.target.value);
62
62
  } })) })) : null, _jsx(Field, { label: copy.email, error: fieldErrors?.email, children: (fieldProps) => (_jsx(EmailInput, { ...fieldProps, name: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, className: inputClass, value: email, disabled: pending, onChange: (event) => {
@@ -9,7 +9,7 @@ export interface PodSignInFrameProps {
9
9
  * service name.
10
10
  */
11
11
  appIntro?: ReactNode;
12
- /** The 360px wide body. */
12
+ /** A responsive body, up to 480px wide. */
13
13
  children: ReactNode;
14
14
  /**
15
15
  * `dialog` only. `true` (default) draws a modal layer over the document;
@@ -25,7 +25,7 @@ export interface PodSignInFrameProps {
25
25
  }
26
26
  /**
27
27
  * Outer frame of every sign-in surface. `window` fills the host window,
28
- * `dialog` is a 400px layer, `page` is two columns. All three share the same
29
- * 360px body, so a state looks the same wherever the host places it.
28
+ * `dialog` is a 480px layer, `page` is two columns. All three share the same
29
+ * responsive body, so narrower hosts keep the same controls and hierarchy.
30
30
  */
31
31
  export declare function PodSignInFrame({ presentation, ariaLabel, appIntro, children, modal, onClose, closeLabel, dataAttributes, }: PodSignInFrameProps): import("react").JSX.Element;
@@ -90,11 +90,11 @@ function useModalFocus(active, onClose) {
90
90
  }, [active]);
91
91
  return ref;
92
92
  }
93
- const bodyClass = 'flex min-h-0 w-full max-w-[360px] flex-1 flex-col';
93
+ const bodyClass = 'flex min-h-0 w-full max-w-[480px] flex-1 flex-col';
94
94
  /**
95
95
  * Outer frame of every sign-in surface. `window` fills the host window,
96
- * `dialog` is a 400px layer, `page` is two columns. All three share the same
97
- * 360px body, so a state looks the same wherever the host places it.
96
+ * `dialog` is a 480px layer, `page` is two columns. All three share the same
97
+ * responsive body, so narrower hosts keep the same controls and hierarchy.
98
98
  */
99
99
  export function PodSignInFrame({ presentation, ariaLabel, appIntro, children, modal = true, onClose, closeLabel, dataAttributes, }) {
100
100
  const isModal = presentation === 'dialog' && modal;
@@ -104,8 +104,8 @@ export function PodSignInFrame({ presentation, ariaLabel, appIntro, children, mo
104
104
  return (_jsx("div", { role: "region", "aria-label": ariaLabel, "data-pod-sign-in-frame": "window", ...dataAttributes, className: "pod-sign-in flex h-full min-h-0 w-full min-w-0 justify-center overflow-hidden bg-background text-foreground", children: _jsx("div", { className: bodyClass, children: children }) }));
105
105
  }
106
106
  if (presentation === 'page') {
107
- return (_jsxs("div", { role: "region", "aria-label": ariaLabel, "data-pod-sign-in-frame": "page", ...dataAttributes, className: "pod-sign-in grid min-h-[100dvh] w-full bg-background text-foreground md:grid-cols-2", children: [intro ? (_jsx("aside", { "data-pod-sign-in": "intro", className: "hidden flex-col justify-center gap-6 bg-[hsl(var(--sunken))] px-12 py-10 md:flex", children: intro })) : null, _jsx("div", { className: cn('flex min-w-0 items-center justify-center px-4 py-8', intro ? '' : 'md:col-span-2'), children: _jsx("div", { className: bodyClass, children: children }) })] }));
107
+ return (_jsxs("div", { role: "region", "aria-label": ariaLabel, "data-pod-sign-in-frame": "page", ...dataAttributes, className: "pod-sign-in grid min-h-[100dvh] w-full bg-background text-foreground md:grid-cols-2", children: [intro ? (_jsx("aside", { "data-pod-sign-in": "intro", className: "hidden flex-col justify-center gap-6 bg-[hsl(var(--sunken))] px-12 py-10 md:flex", children: intro })) : null, _jsx("div", { className: cn('flex min-w-0 items-center justify-center px-4 py-8', intro ? '' : 'md:col-span-2'), children: _jsx("div", { className: cn(bodyClass, 'min-h-[520px]'), children: children }) })] }));
108
108
  }
109
- const card = (_jsxs("div", { ref: dialogRef, role: isModal ? 'dialog' : 'region', "aria-modal": isModal ? true : undefined, "aria-label": ariaLabel, tabIndex: -1, "data-pod-sign-in-frame": "dialog", ...dataAttributes, className: cn('pod-sign-in relative flex max-h-[90dvh] w-[min(400px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-lg focus:outline-none motion-safe:animate-in motion-safe:fade-in-0'), children: [isModal && onClose && closeLabel ? (_jsx("button", { type: "button", "aria-label": closeLabel, className: "absolute right-2 top-2 flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent", onClick: onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })) : null, _jsx("div", { className: cn(bodyClass, 'mx-auto'), children: children })] }));
109
+ const card = (_jsxs("div", { ref: dialogRef, role: isModal ? 'dialog' : 'region', "aria-modal": isModal ? true : undefined, "aria-label": ariaLabel, tabIndex: -1, "data-pod-sign-in-frame": "dialog", ...dataAttributes, className: cn('pod-sign-in relative flex min-h-[min(440px,90dvh)] max-h-[90dvh] w-[min(480px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-lg focus:outline-none motion-safe:animate-in motion-safe:fade-in-0'), children: [isModal && onClose && closeLabel ? (_jsx("button", { type: "button", "aria-label": closeLabel, className: "absolute right-2 top-2 flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent", onClick: onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })) : null, _jsx("div", { className: cn(bodyClass, 'mx-auto'), children: children })] }));
110
110
  return isModal ? (_jsx("div", { className: "fixed inset-0 z-[var(--layer-modal)] flex items-center justify-center bg-background/80 p-4", children: card })) : (_jsx("div", { className: "flex w-full items-center justify-center p-4", children: card }));
111
111
  }
@@ -2,11 +2,11 @@ import { type ReactNode } from 'react';
2
2
  import { type ButtonProps } from '../button.js';
3
3
  import { type FormFieldProps } from '../form-field.js';
4
4
  import type { StorageLocation } from './types.js';
5
- /** Minimum hit targets (spec §5); content may grow when text is enlarged or wraps. */
6
- export declare const primaryButtonClass = "h-auto min-h-11 w-full whitespace-normal break-normal rounded-lg py-2 text-sm font-medium leading-snug";
7
- export declare const outlineButtonClass = "h-auto min-h-11 w-full whitespace-normal break-normal rounded-lg py-2 text-sm font-medium leading-snug";
8
- export declare const textButtonClass = "h-auto min-h-9 max-w-full whitespace-normal break-normal rounded-lg px-2 py-1 text-sm font-medium leading-snug";
9
- export declare const fieldClass = "h-11 w-full rounded-lg border border-input bg-card px-3 text-sm text-foreground placeholder:text-muted-foreground disabled:opacity-60";
5
+ /** Screen-level controls leave room for readable text and touch targets. */
6
+ export declare const primaryButtonClass = "h-auto min-h-12 w-full whitespace-normal break-normal rounded-lg py-2 text-base font-medium leading-snug";
7
+ export declare const outlineButtonClass = "h-auto min-h-12 w-full whitespace-normal break-normal rounded-lg py-2 text-base font-medium leading-snug";
8
+ export declare const textButtonClass = "h-9 rounded-lg px-2 text-sm font-medium";
9
+ export declare const fieldClass = "h-12 w-full rounded-lg border border-input bg-card px-4 text-base text-foreground placeholder:text-muted-foreground disabled:opacity-60";
10
10
  export declare function Spinner({ className }: {
11
11
  className?: string;
12
12
  }): import("react").JSX.Element;
@@ -7,11 +7,11 @@ import { cn } from '../utils.js';
7
7
  import { FormField } from '../form-field.js';
8
8
  import { Checkbox } from '../checkbox.js';
9
9
  import { StorageBadge } from './StorageBadge.js';
10
- /** Minimum hit targets (spec §5); content may grow when text is enlarged or wraps. */
11
- export const primaryButtonClass = 'h-auto min-h-11 w-full whitespace-normal break-normal rounded-lg py-2 text-sm font-medium leading-snug';
10
+ /** Screen-level controls leave room for readable text and touch targets. */
11
+ export const primaryButtonClass = 'h-auto min-h-12 w-full whitespace-normal break-normal rounded-lg py-2 text-base font-medium leading-snug';
12
12
  export const outlineButtonClass = primaryButtonClass;
13
- export const textButtonClass = 'h-auto min-h-9 max-w-full whitespace-normal break-normal rounded-lg px-2 py-1 text-sm font-medium leading-snug';
14
- export const fieldClass = 'h-11 w-full rounded-lg border border-input bg-card px-3 text-sm text-foreground placeholder:text-muted-foreground disabled:opacity-60';
13
+ export const textButtonClass = 'h-9 rounded-lg px-2 text-sm font-medium';
14
+ export const fieldClass = 'h-12 w-full rounded-lg border border-input bg-card px-4 text-base text-foreground placeholder:text-muted-foreground disabled:opacity-60';
15
15
  export function Spinner({ className }) {
16
16
  return _jsx(Loader2, { "aria-hidden": "true", className: cn('h-4 w-4 shrink-0 animate-spin motion-reduce:animate-none', className) });
17
17
  }
@@ -55,7 +55,7 @@ export function usePastDelay(delayMs) {
55
55
  * Pass `onSubmit` to make the whole screen one form (actions submit it).
56
56
  */
57
57
  export function ScreenLayout({ chrome, children, actions, onSubmit, mainClassName, ...state }) {
58
- const inner = (_jsxs(_Fragment, { children: [chrome, _jsx("div", { "data-pod-sign-in": "main", className: cn('flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto px-6 pb-2 pt-5', mainClassName), children: children }), actions ? _jsx("div", { "data-pod-sign-in": "actions", className: "flex shrink-0 flex-col gap-3 px-6 pb-6 pt-3", children: actions }) : null] }));
58
+ const inner = (_jsxs(_Fragment, { children: [chrome, _jsx("div", { "data-pod-sign-in": "main", className: cn('flex min-h-0 flex-1 flex-col gap-7 overflow-y-auto px-6 pb-3 pt-7 min-[400px]:px-8 min-[400px]:pt-8', mainClassName), children: children }), actions ? _jsx("div", { "data-pod-sign-in": "actions", className: "flex shrink-0 flex-col gap-4 px-6 pb-7 pt-5 min-[400px]:px-8 min-[400px]:pb-8", children: actions }) : null] }));
59
59
  const className = 'flex min-h-0 flex-1 flex-col';
60
60
  return onSubmit
61
61
  ? _jsx("form", { ...state, onSubmit: onSubmit, className: className, noValidate: true, children: inner })