@undefineds.co/xpod 0.4.29 → 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.
- package/dist/api/ApiServer.js +4 -0
- package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
- package/dist/api/ai-gateway/AiGatewayService.js +145 -6
- package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
- package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
- package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
- package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
- package/dist/api/ai-gateway/connect/index.d.ts +40 -0
- package/dist/api/ai-gateway/connect/index.js +194 -4
- package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
- package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
- package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
- package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
- package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
- package/dist/api/auth/AuthContext.d.ts +4 -0
- package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
- package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
- package/dist/api/auth/SolidSessionFactory.js +114 -4
- package/dist/api/chatkit/pod-store.d.ts +8 -1
- package/dist/api/chatkit/pod-store.js +54 -3
- package/dist/api/container/common.js +34 -60
- package/dist/api/container/index.js +0 -3
- package/dist/api/container/routes.js +1 -8
- package/dist/api/container/types.d.ts +2 -9
- package/dist/api/container/types.js +0 -5
- package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
- package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
- package/dist/api/middleware/AuthMiddleware.js +29 -1
- package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
- package/dist/cli/index.js +2 -1
- package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
- package/dist/runtime/ingress-port.d.ts +9 -2
- package/dist/runtime/ingress-port.js +7 -2
- package/dist/runtime/local-secret-file.d.ts +1 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
- package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
- package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
- package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
- package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
- package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
- package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
- package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
- package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
- package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
- package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
- package/package.json +2 -2
- package/static/app/assets/main.css +1 -1
- package/static/app/assets/main.js +18 -18
- package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
- package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
- package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
- package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
- package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
- package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
- package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
- package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
- package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
- package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
- package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
- package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
- package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
- package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
- package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
- package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
- package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
- package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
- package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
- package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
- package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
- package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
- package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
- package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
- package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
- package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
- package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
- package/static/dashboard/dashboard.html +2 -2
- package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
- package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
- package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
- package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
- package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
- package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
- package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
- package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
- package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
- package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
- package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
- package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
- package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
- package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
- package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
- package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
- package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
- package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
- package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
- package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
- package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
- package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
- package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
- package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
- package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
- package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
- package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
- package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
- package/static/settings/auth-callback.html +3 -3
- package/static/settings/settings.html +3 -3
- package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
- package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
- package/dist/runtime/gateway-locator-secret.d.ts +0 -5
- package/dist/runtime/gateway-locator-secret.js +0 -30
- package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
- package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
- package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
- package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
- package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
- package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
- package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
- package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
- package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
- package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
- package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
- package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
- 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
|
|
30
|
-
if (!
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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. */
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
|
@@ -11,5 +11,5 @@ export interface IdpChromeProps {
|
|
|
11
11
|
serviceLabel?: string;
|
|
12
12
|
locale?: PodSignInLocale;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
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
|
-
/**
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
/**
|
|
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
|
|
29
|
-
*
|
|
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-[
|
|
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
|
|
97
|
-
*
|
|
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(
|
|
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
|
-
/**
|
|
6
|
-
export declare const primaryButtonClass = "h-auto min-h-
|
|
7
|
-
export declare const outlineButtonClass = "h-auto min-h-
|
|
8
|
-
export declare const textButtonClass = "h-
|
|
9
|
-
export declare const fieldClass = "h-
|
|
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
|
-
/**
|
|
11
|
-
export const primaryButtonClass = 'h-auto min-h-
|
|
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-
|
|
14
|
-
export const fieldClass = 'h-
|
|
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-
|
|
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 })
|