@kortix/sdk 0.12.7 → 0.13.0
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/README.md +13 -6
- package/dist/browser/session-sync/session-sync-registry.d.ts +7 -2
- package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.js +33 -44
- package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
- package/dist/browser/stores/opencode-compaction-store.d.ts +9 -3
- package/dist/browser/stores/opencode-compaction-store.d.ts.map +1 -1
- package/dist/browser/stores/opencode-compaction-store.js +5 -12
- package/dist/browser/stores/opencode-compaction-store.js.map +1 -1
- package/dist/browser/stores/sandbox-connection-store.d.ts +28 -17
- package/dist/browser/stores/sandbox-connection-store.d.ts.map +1 -1
- package/dist/browser/stores/sandbox-connection-store.js +32 -84
- package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
- package/dist/browser/stores/session-working-store.d.ts +73 -0
- package/dist/browser/stores/session-working-store.d.ts.map +1 -0
- package/dist/browser/stores/session-working-store.js +86 -0
- package/dist/browser/stores/session-working-store.js.map +1 -0
- package/dist/browser/stores/sync-store/types.d.ts +14 -4
- package/dist/browser/stores/sync-store/types.d.ts.map +1 -1
- package/dist/browser/stores/sync-store.d.ts +140 -3
- package/dist/browser/stores/sync-store.d.ts.map +1 -1
- package/dist/browser/stores/sync-store.js +425 -36
- package/dist/browser/stores/sync-store.js.map +1 -1
- package/dist/core/client/kortix.d.ts +102 -1
- package/dist/core/client/kortix.d.ts.map +1 -1
- package/dist/core/client/kortix.js +78 -0
- package/dist/core/client/kortix.js.map +1 -1
- package/dist/core/files/client.d.ts +50 -2
- package/dist/core/files/client.d.ts.map +1 -1
- package/dist/core/files/client.js +253 -44
- package/dist/core/files/client.js.map +1 -1
- package/dist/core/files/types.d.ts +11 -0
- package/dist/core/files/types.d.ts.map +1 -1
- package/dist/core/http/abort-error.d.ts +89 -0
- package/dist/core/http/abort-error.d.ts.map +1 -0
- package/dist/core/http/abort-error.js +190 -0
- package/dist/core/http/abort-error.js.map +1 -0
- package/dist/core/http/api-client.d.ts.map +1 -1
- package/dist/core/http/api-client.js +28 -4
- package/dist/core/http/api-client.js.map +1 -1
- package/dist/core/http/impersonation.d.ts +58 -0
- package/dist/core/http/impersonation.d.ts.map +1 -0
- package/dist/core/http/impersonation.js +174 -0
- package/dist/core/http/impersonation.js.map +1 -0
- package/dist/core/rest/platform-client/github-app.d.ts +13 -0
- package/dist/core/rest/platform-client/github-app.d.ts.map +1 -1
- package/dist/core/rest/platform-client/github-app.js +2 -0
- package/dist/core/rest/platform-client/github-app.js.map +1 -1
- package/dist/core/rest/platform-client/members.d.ts +10 -64
- package/dist/core/rest/platform-client/members.d.ts.map +1 -1
- package/dist/core/rest/platform-client/members.js +10 -28
- package/dist/core/rest/platform-client/members.js.map +1 -1
- package/dist/core/rest/projects-client/accounts.d.ts +30 -1
- package/dist/core/rest/projects-client/accounts.d.ts.map +1 -1
- package/dist/core/rest/projects-client/accounts.js +1 -1
- package/dist/core/rest/projects-client/accounts.js.map +1 -1
- package/dist/core/rest/projects-client/agent-config.d.ts +21 -0
- package/dist/core/rest/projects-client/agent-config.d.ts.map +1 -1
- package/dist/core/rest/projects-client/agent-config.js +14 -0
- package/dist/core/rest/projects-client/agent-config.js.map +1 -1
- package/dist/core/rest/projects-client/apps.d.ts +13 -0
- package/dist/core/rest/projects-client/apps.d.ts.map +1 -1
- package/dist/core/rest/projects-client/apps.js.map +1 -1
- package/dist/core/rest/projects-client/assignments.d.ts +114 -0
- package/dist/core/rest/projects-client/assignments.d.ts.map +1 -0
- package/dist/core/rest/projects-client/assignments.js +91 -0
- package/dist/core/rest/projects-client/assignments.js.map +1 -0
- package/dist/core/rest/projects-client/billing.d.ts +80 -1
- package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
- package/dist/core/rest/projects-client/billing.js +61 -2
- package/dist/core/rest/projects-client/billing.js.map +1 -1
- package/dist/core/rest/projects-client/connectors.d.ts +2 -0
- package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
- package/dist/core/rest/projects-client/connectors.js.map +1 -1
- package/dist/core/rest/projects-client/files.js +2 -2
- package/dist/core/rest/projects-client/files.js.map +1 -1
- package/dist/core/rest/projects-client/gateway.d.ts +51 -0
- package/dist/core/rest/projects-client/gateway.d.ts.map +1 -1
- package/dist/core/rest/projects-client/gateway.js.map +1 -1
- package/dist/core/rest/projects-client/iam.d.ts +51 -3
- package/dist/core/rest/projects-client/iam.d.ts.map +1 -1
- package/dist/core/rest/projects-client/iam.js +18 -1
- package/dist/core/rest/projects-client/iam.js.map +1 -1
- package/dist/core/rest/projects-client/index.d.ts +1 -0
- package/dist/core/rest/projects-client/index.d.ts.map +1 -1
- package/dist/core/rest/projects-client/index.js +1 -0
- package/dist/core/rest/projects-client/index.js.map +1 -1
- package/dist/core/rest/projects-client/projects.d.ts +1 -1
- package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
- package/dist/core/rest/projects-client/projects.js +3 -0
- package/dist/core/rest/projects-client/projects.js.map +1 -1
- package/dist/core/rest/projects-client/secrets.d.ts +11 -0
- package/dist/core/rest/projects-client/secrets.d.ts.map +1 -1
- package/dist/core/rest/projects-client/secrets.js +1 -1
- package/dist/core/rest/projects-client/secrets.js.map +1 -1
- package/dist/core/rest/projects-client/session-costs.d.ts +33 -0
- package/dist/core/rest/projects-client/session-costs.d.ts.map +1 -1
- package/dist/core/rest/projects-client/session-costs.js.map +1 -1
- package/dist/core/rest/projects-client/sessions.d.ts +253 -4
- package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sessions.js +101 -4
- package/dist/core/rest/projects-client/sessions.js.map +1 -1
- package/dist/core/rest/projects-client/shared.d.ts +10 -1
- package/dist/core/rest/projects-client/shared.d.ts.map +1 -1
- package/dist/core/rest/projects-client/shared.js.map +1 -1
- package/dist/core/rest/projects-client/tokens.d.ts +13 -1
- package/dist/core/rest/projects-client/tokens.d.ts.map +1 -1
- package/dist/core/rest/projects-client/tokens.js +7 -2
- package/dist/core/rest/projects-client/tokens.js.map +1 -1
- package/dist/core/rest/projects-client/triggers.d.ts +69 -2
- package/dist/core/rest/projects-client/triggers.d.ts.map +1 -1
- package/dist/core/rest/projects-client/triggers.js +1 -1
- package/dist/core/rest/projects-client/triggers.js.map +1 -1
- package/dist/core/session/compaction.d.ts +98 -0
- package/dist/core/session/compaction.d.ts.map +1 -0
- package/dist/core/session/compaction.js +108 -0
- package/dist/core/session/compaction.js.map +1 -0
- package/dist/core/session/health.d.ts +22 -0
- package/dist/core/session/health.d.ts.map +1 -1
- package/dist/core/session/health.js +31 -5
- package/dist/core/session/health.js.map +1 -1
- package/dist/core/session/index.d.ts +2 -0
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js +2 -0
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/message-queue.d.ts +79 -15
- package/dist/core/session/message-queue.d.ts.map +1 -1
- package/dist/core/session/message-queue.js +73 -27
- package/dist/core/session/message-queue.js.map +1 -1
- package/dist/core/session/rewind.d.ts +76 -10
- package/dist/core/session/rewind.d.ts.map +1 -1
- package/dist/core/session/rewind.js +68 -11
- package/dist/core/session/rewind.js.map +1 -1
- package/dist/core/session/working.d.ts +241 -0
- package/dist/core/session/working.d.ts.map +1 -0
- package/dist/core/session/working.js +261 -0
- package/dist/core/session/working.js.map +1 -0
- package/dist/core/session-sync/session-sync-controller.d.ts +35 -20
- package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
- package/dist/core/session-sync/session-sync-controller.js +48 -115
- package/dist/core/session-sync/session-sync-controller.js.map +1 -1
- package/dist/core/stream/event-stream.d.ts +22 -0
- package/dist/core/stream/event-stream.d.ts.map +1 -1
- package/dist/core/stream/event-stream.js +132 -36
- package/dist/core/stream/event-stream.js.map +1 -1
- package/dist/core/turns/classify.d.ts +5 -2
- package/dist/core/turns/classify.d.ts.map +1 -1
- package/dist/core/turns/classify.js +1 -0
- package/dist/core/turns/classify.js.map +1 -1
- package/dist/core/turns/errors.d.ts +11 -0
- package/dist/core/turns/errors.d.ts.map +1 -1
- package/dist/core/turns/errors.js +49 -2
- package/dist/core/turns/errors.js.map +1 -1
- package/dist/core/turns/grouping.d.ts +32 -2
- package/dist/core/turns/grouping.d.ts.map +1 -1
- package/dist/core/turns/grouping.js +31 -2
- package/dist/core/turns/grouping.js.map +1 -1
- package/dist/core/turns/index.d.ts +1 -0
- package/dist/core/turns/index.d.ts.map +1 -1
- package/dist/core/turns/index.js +1 -0
- package/dist/core/turns/index.js.map +1 -1
- package/dist/core/turns/open-turn.d.ts +81 -0
- package/dist/core/turns/open-turn.d.ts.map +1 -0
- package/dist/core/turns/open-turn.js +77 -0
- package/dist/core/turns/open-turn.js.map +1 -0
- package/dist/core/turns/state.d.ts +1 -1
- package/dist/core/turns/state.d.ts.map +1 -1
- package/dist/core/turns/state.js +29 -27
- package/dist/core/turns/state.js.map +1 -1
- package/dist/core/turns/types.d.ts +8 -0
- package/dist/core/turns/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/kortix.esm.min.js +25 -25
- package/dist/kortix.global.js +25 -25
- package/dist/node/server.d.ts.map +1 -1
- package/dist/node/server.js +14 -2
- package/dist/node/server.js.map +1 -1
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/initial-session-pin.d.ts +17 -0
- package/dist/react/initial-session-pin.d.ts.map +1 -1
- package/dist/react/initial-session-pin.js +58 -0
- package/dist/react/initial-session-pin.js.map +1 -1
- package/dist/react/opencode.d.ts +6 -2
- package/dist/react/opencode.d.ts.map +1 -1
- package/dist/react/opencode.js +5 -1
- package/dist/react/opencode.js.map +1 -1
- package/dist/react/prefetch-session-start.d.ts +10 -4
- package/dist/react/prefetch-session-start.d.ts.map +1 -1
- package/dist/react/prefetch-session-start.js +10 -4
- package/dist/react/prefetch-session-start.js.map +1 -1
- package/dist/react/query-keys.d.ts +8 -2
- package/dist/react/query-keys.d.ts.map +1 -1
- package/dist/react/query-keys.js +8 -2
- package/dist/react/query-keys.js.map +1 -1
- package/dist/react/session-start-stash.d.ts +25 -0
- package/dist/react/session-start-stash.d.ts.map +1 -1
- package/dist/react/session-start-stash.js +34 -3
- package/dist/react/session-start-stash.js.map +1 -1
- package/dist/react/session-title-sync.d.ts +5 -1
- package/dist/react/session-title-sync.d.ts.map +1 -1
- package/dist/react/session-title-sync.js +16 -4
- package/dist/react/session-title-sync.js.map +1 -1
- package/dist/react/use-admin-accounts.d.ts +121 -0
- package/dist/react/use-admin-accounts.d.ts.map +1 -1
- package/dist/react/use-admin-accounts.js +92 -0
- package/dist/react/use-admin-accounts.js.map +1 -1
- package/dist/react/use-can.d.ts +105 -0
- package/dist/react/use-can.d.ts.map +1 -0
- package/dist/react/use-can.js +190 -0
- package/dist/react/use-can.js.map +1 -0
- package/dist/react/use-canonical-opencode-session.d.ts +6 -1
- package/dist/react/use-canonical-opencode-session.d.ts.map +1 -1
- package/dist/react/use-canonical-opencode-session.js +46 -3
- package/dist/react/use-canonical-opencode-session.js.map +1 -1
- package/dist/react/use-impersonation.d.ts +54 -0
- package/dist/react/use-impersonation.d.ts.map +1 -0
- package/dist/react/use-impersonation.js +91 -0
- package/dist/react/use-impersonation.js.map +1 -0
- package/dist/react/use-opencode-events/handle-event.d.ts +10 -0
- package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
- package/dist/react/use-opencode-events/handle-event.js +76 -7
- package/dist/react/use-opencode-events/handle-event.js.map +1 -1
- package/dist/react/use-opencode-events/helpers.d.ts +21 -8
- package/dist/react/use-opencode-events/helpers.d.ts.map +1 -1
- package/dist/react/use-opencode-events/helpers.js +25 -26
- package/dist/react/use-opencode-events/helpers.js.map +1 -1
- package/dist/react/use-opencode-events/index.d.ts.map +1 -1
- package/dist/react/use-opencode-events/index.js +43 -26
- package/dist/react/use-opencode-events/index.js.map +1 -1
- package/dist/react/use-opencode-events/use-event-stream-refs.d.ts.map +1 -1
- package/dist/react/use-opencode-events/use-event-stream-refs.js +66 -3
- package/dist/react/use-opencode-events/use-event-stream-refs.js.map +1 -1
- package/dist/react/use-opencode-local.d.ts +17 -0
- package/dist/react/use-opencode-local.d.ts.map +1 -1
- package/dist/react/use-opencode-local.js +41 -7
- package/dist/react/use-opencode-local.js.map +1 -1
- package/dist/react/use-opencode-sessions/messages.d.ts +101 -1
- package/dist/react/use-opencode-sessions/messages.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/messages.js +481 -63
- package/dist/react/use-opencode-sessions/messages.js.map +1 -1
- package/dist/react/use-project-apps.d.ts +24 -1
- package/dist/react/use-project-apps.d.ts.map +1 -1
- package/dist/react/use-project-apps.js +5 -3
- package/dist/react/use-project-apps.js.map +1 -1
- package/dist/react/use-runtime-boot-stalled.d.ts +32 -0
- package/dist/react/use-runtime-boot-stalled.d.ts.map +1 -0
- package/dist/react/use-runtime-boot-stalled.js +48 -0
- package/dist/react/use-runtime-boot-stalled.js.map +1 -0
- package/dist/react/use-runtime-reconnect.d.ts +67 -1
- package/dist/react/use-runtime-reconnect.d.ts.map +1 -1
- package/dist/react/use-runtime-reconnect.js +116 -18
- package/dist/react/use-runtime-reconnect.js.map +1 -1
- package/dist/react/use-session-prompts.d.ts +113 -0
- package/dist/react/use-session-prompts.d.ts.map +1 -0
- package/dist/react/use-session-prompts.js +242 -0
- package/dist/react/use-session-prompts.js.map +1 -0
- package/dist/react/use-session-send.d.ts +169 -20
- package/dist/react/use-session-send.d.ts.map +1 -1
- package/dist/react/use-session-send.js +178 -29
- package/dist/react/use-session-send.js.map +1 -1
- package/dist/react/use-session-sync.d.ts +35 -2
- package/dist/react/use-session-sync.d.ts.map +1 -1
- package/dist/react/use-session-sync.js +46 -9
- package/dist/react/use-session-sync.js.map +1 -1
- package/dist/react/use-session-working.d.ts +57 -0
- package/dist/react/use-session-working.d.ts.map +1 -0
- package/dist/react/use-session-working.js +178 -0
- package/dist/react/use-session-working.js.map +1 -0
- package/dist/react/use-session.d.ts +122 -5
- package/dist/react/use-session.d.ts.map +1 -1
- package/dist/react/use-session.js +343 -69
- package/dist/react/use-session.js.map +1 -1
- package/package.json +2 -2
- package/dist/browser/stores/tab-store.d.ts +0 -91
- package/dist/browser/stores/tab-store.d.ts.map +0 -1
- package/dist/browser/stores/tab-store.js +0 -369
- package/dist/browser/stores/tab-store.js.map +0 -1
- package/dist/core/session/send-queue.d.ts +0 -70
- package/dist/core/session/send-queue.d.ts.map +0 -1
- package/dist/core/session/send-queue.js +0 -116
- package/dist/core/session/send-queue.js.map +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* `useCan` / `useCans` — the ONLY client-side authorization read.
|
|
4
|
+
*
|
|
5
|
+
* Relocated from `apps/web/src/lib/use-permission.ts` + `use-project-can.ts`.
|
|
6
|
+
* They were host-local, so every other host (the whitelabel demo, mobile) had
|
|
7
|
+
* no probe at all and fell back to reading a role literal off a row — which is
|
|
8
|
+
* exactly the bug the canonical model deletes: a member holding a custom role
|
|
9
|
+
* that grants `project.members.manage` is invisible to `role === 'manager'`.
|
|
10
|
+
*
|
|
11
|
+
* Ask for the LEAF the route asserts (`project.gitops.push`), never for a role.
|
|
12
|
+
* The engine answers; the client renders the answer.
|
|
13
|
+
*
|
|
14
|
+
* NOT a security boundary — the API re-checks every mutating route. This only
|
|
15
|
+
* decides what to show and enable.
|
|
16
|
+
*
|
|
17
|
+
* ### Cache contract
|
|
18
|
+
*
|
|
19
|
+
* A verdict is cached for 5 minutes. That is only safe because every
|
|
20
|
+
* assignment write calls `invalidatePermissionProbes`, so a revoke is visible
|
|
21
|
+
* on the next render rather than up to five minutes later. If you add a write
|
|
22
|
+
* path that changes an assignment, it calls that function. No exceptions.
|
|
23
|
+
*/
|
|
24
|
+
import { useQuery } from '@tanstack/react-query';
|
|
25
|
+
import { getProject, probeEffectivePermission, probeEffectivePermissions, validateToken, } from '../core/rest/projects-client/index.js';
|
|
26
|
+
import { contract } from './query-contracts.js';
|
|
27
|
+
import { qk } from './query-keys.js';
|
|
28
|
+
const PROBE_STALE_MS = 5 * 60_000;
|
|
29
|
+
/** The single-probe cache key. Kept as a flat literal (not under `qk`) because
|
|
30
|
+
* it predates that factory and both `apps/web` and its tests address it. */
|
|
31
|
+
export const permissionProbeKey = (accountId, userId, action, target) => [
|
|
32
|
+
'iam-permission',
|
|
33
|
+
accountId,
|
|
34
|
+
userId,
|
|
35
|
+
action,
|
|
36
|
+
target?.resourceType ?? null,
|
|
37
|
+
target?.resourceId ?? null,
|
|
38
|
+
];
|
|
39
|
+
/** The batch cache key. Keyed on the probe LIST so adding or removing a probe
|
|
40
|
+
* refetches; the signature is action+target per probe, never object identity,
|
|
41
|
+
* which React swaps every render. */
|
|
42
|
+
export const permissionProbeBatchKey = (accountId, userId, probes) => [
|
|
43
|
+
'iam-permission-batch',
|
|
44
|
+
accountId,
|
|
45
|
+
userId,
|
|
46
|
+
probes.map((p) => `${p.action}|${p.resourceType ?? ''}|${p.resourceId ?? ''}`).join(','),
|
|
47
|
+
];
|
|
48
|
+
/** Identity of the calling principal, for hosts that do not inject one. */
|
|
49
|
+
const identityKey = ['iam-identity', 'me'];
|
|
50
|
+
function useProbeUserId(explicit) {
|
|
51
|
+
const query = useQuery({
|
|
52
|
+
queryKey: identityKey,
|
|
53
|
+
queryFn: async () => (await validateToken()).identity ?? null,
|
|
54
|
+
enabled: !explicit,
|
|
55
|
+
staleTime: Infinity,
|
|
56
|
+
});
|
|
57
|
+
return explicit ?? query.data?.user_id;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the account that owns a project. A DISABLED react-query query reports
|
|
61
|
+
* `isLoading === false` in v5, so callers must not read the unresolved window
|
|
62
|
+
* as a denial — `pending()` below converts it to "still loading".
|
|
63
|
+
*/
|
|
64
|
+
function useOwningAccountId(projectId, accountIdHint) {
|
|
65
|
+
const { data } = useQuery({
|
|
66
|
+
queryKey: qk.project.summary(projectId ?? ''),
|
|
67
|
+
queryFn: () => getProject(projectId),
|
|
68
|
+
enabled: !!projectId && !accountIdHint,
|
|
69
|
+
...contract('config'),
|
|
70
|
+
});
|
|
71
|
+
return projectId ? (accountIdHint ?? data?.account_id) : undefined;
|
|
72
|
+
}
|
|
73
|
+
const pending = () => ({
|
|
74
|
+
allowed: false,
|
|
75
|
+
reason: null,
|
|
76
|
+
isLoading: true,
|
|
77
|
+
isError: false,
|
|
78
|
+
});
|
|
79
|
+
/** Everything a target resolves to: the account to ask, and the probe target. */
|
|
80
|
+
function useResolvedTarget(target) {
|
|
81
|
+
const isProject = 'projectId' in target && target.projectId !== undefined;
|
|
82
|
+
const projectId = 'projectId' in target ? target.projectId : undefined;
|
|
83
|
+
const owningAccountId = useOwningAccountId(projectId, target.accountId);
|
|
84
|
+
const accountId = isProject ? owningAccountId : target.accountId;
|
|
85
|
+
const probeTarget = isProject
|
|
86
|
+
? { resourceType: 'project', resourceId: projectId }
|
|
87
|
+
: target.resource
|
|
88
|
+
? { resourceType: target.resource.type, resourceId: target.resource.id }
|
|
89
|
+
: undefined;
|
|
90
|
+
return { accountId, probeTarget, resolved: !!accountId && (!isProject || !!projectId) };
|
|
91
|
+
}
|
|
92
|
+
/** Can this principal perform `action` on this target? */
|
|
93
|
+
export function useCan(target, action, options) {
|
|
94
|
+
const userId = useProbeUserId(options?.userId);
|
|
95
|
+
const { accountId, probeTarget, resolved } = useResolvedTarget(target);
|
|
96
|
+
const enabled = resolved && !!userId;
|
|
97
|
+
const query = useQuery({
|
|
98
|
+
queryKey: permissionProbeKey(accountId, userId, action, probeTarget),
|
|
99
|
+
queryFn: () => probeEffectivePermission(accountId, userId, (probeTarget ? { action, ...probeTarget } : { action })),
|
|
100
|
+
enabled,
|
|
101
|
+
staleTime: PROBE_STALE_MS,
|
|
102
|
+
});
|
|
103
|
+
if (!enabled)
|
|
104
|
+
return pending();
|
|
105
|
+
return {
|
|
106
|
+
allowed: query.data?.allowed === true,
|
|
107
|
+
reason: query.data?.reason ?? null,
|
|
108
|
+
isLoading: query.isLoading,
|
|
109
|
+
isError: query.isError,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Batch variant — ONE roundtrip for N actions on the same target. Returns a map
|
|
114
|
+
* keyed by action, so a caller reads `caps[ACTION].allowed`.
|
|
115
|
+
*
|
|
116
|
+
* `actions` must be stable across renders (module-level or memoized): the query
|
|
117
|
+
* key is derived from the list.
|
|
118
|
+
*/
|
|
119
|
+
export function useCans(target, actions, options) {
|
|
120
|
+
const userId = useProbeUserId(options?.userId);
|
|
121
|
+
const { accountId, probeTarget, resolved } = useResolvedTarget(target);
|
|
122
|
+
const probes = actions.map((action) => (probeTarget ? { action, ...probeTarget } : { action }));
|
|
123
|
+
const enabled = resolved && !!userId && probes.length > 0;
|
|
124
|
+
const query = useQuery({
|
|
125
|
+
queryKey: permissionProbeBatchKey(accountId, userId, probes),
|
|
126
|
+
queryFn: () => probeEffectivePermissions(accountId, userId, probes),
|
|
127
|
+
enabled,
|
|
128
|
+
staleTime: PROBE_STALE_MS,
|
|
129
|
+
});
|
|
130
|
+
const map = {};
|
|
131
|
+
actions.forEach((action, i) => {
|
|
132
|
+
if (!enabled) {
|
|
133
|
+
map[action] = pending();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const hit = query.data?.[i];
|
|
137
|
+
map[action] = {
|
|
138
|
+
allowed: hit?.allowed === true,
|
|
139
|
+
reason: hit?.reason ?? null,
|
|
140
|
+
isLoading: query.isLoading,
|
|
141
|
+
isError: query.isError,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
return map;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Probe on behalf of ANOTHER member — the "what can this person actually do"
|
|
148
|
+
* admin panels. Returns one result per input, in order. The caller needs
|
|
149
|
+
* `member.read` on the account; the server enforces it.
|
|
150
|
+
*
|
|
151
|
+
* Kept probe-shaped (not action-shaped like `useCans`) because these panels mix
|
|
152
|
+
* account and project targets in one request.
|
|
153
|
+
*/
|
|
154
|
+
export function usePermissionsFor(accountId, memberUserId, probes) {
|
|
155
|
+
const enabled = !!accountId && !!memberUserId && probes.length > 0;
|
|
156
|
+
const query = useQuery({
|
|
157
|
+
queryKey: permissionProbeBatchKey(accountId, memberUserId, probes),
|
|
158
|
+
queryFn: () => probeEffectivePermissions(accountId, memberUserId, probes),
|
|
159
|
+
enabled,
|
|
160
|
+
staleTime: PROBE_STALE_MS,
|
|
161
|
+
});
|
|
162
|
+
return probes.map((_, i) => {
|
|
163
|
+
if (!enabled)
|
|
164
|
+
return pending();
|
|
165
|
+
const hit = query.data?.[i];
|
|
166
|
+
return {
|
|
167
|
+
allowed: hit?.allowed === true,
|
|
168
|
+
reason: hit?.reason ?? null,
|
|
169
|
+
isLoading: query.isLoading,
|
|
170
|
+
isError: query.isError,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Bust the probe cache after an assignment write. **Every** write that creates,
|
|
176
|
+
* changes or revokes an assignment calls this — a 5-minute stale window on an
|
|
177
|
+
* authorization verdict is a revoke that has not happened yet.
|
|
178
|
+
*
|
|
179
|
+
* Scope it as tightly as you honestly can: an account+user pair when you know
|
|
180
|
+
* whose access changed, the account when a group or a role definition changed
|
|
181
|
+
* (that moves verdicts for principals you cannot enumerate client-side), and
|
|
182
|
+
* everything only when you know nothing.
|
|
183
|
+
*/
|
|
184
|
+
export function invalidatePermissionProbes(queryClient, scope) {
|
|
185
|
+
const suffix = [scope.accountId, scope.userId].filter((s) => !!s);
|
|
186
|
+
queryClient.invalidateQueries({ queryKey: ['iam-permission', ...suffix] });
|
|
187
|
+
queryClient.invalidateQueries({ queryKey: ['iam-permission-batch', ...suffix] });
|
|
188
|
+
return Promise.resolve();
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=use-can.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-can.js","sourceRoot":"","sources":["../../src/react/use-can.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,12 @@ import { type Session } from './use-opencode-sessions/index.js';
|
|
|
8
8
|
* the client (the old client-side `ensure-opencode` mutation caused the
|
|
9
9
|
* "session replaced / data lost" drift). It just surfaces the pin:
|
|
10
10
|
* 1. the value /start handed us this render (`pinFromStart`), else
|
|
11
|
-
* 2. the
|
|
11
|
+
* 2. the host's own warm-list pin (`initialPin`), else
|
|
12
|
+
* 3. the persisted row (`getProjectSession`) once its REST read resolves,
|
|
13
|
+
* or — before it does — this browser's own synchronous local mirror of
|
|
14
|
+
* the last canonical id seen for this session (T6). The local
|
|
15
|
+
* mirror exists ONLY to cover the gap before (3)'s network read lands;
|
|
16
|
+
* it never outranks it once loaded, and neither ever outranks (1).
|
|
12
17
|
*
|
|
13
18
|
* The OpenCode session list is still read (read-only) for ?oc deep-links and
|
|
14
19
|
* sidebar sub-session rendering.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-canonical-opencode-session.d.ts","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-canonical-opencode-session.d.ts","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"AAUA,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAa5E;;;;;;;;;;;;;;;;;;GAkBG;AAEH;gFACgF;AAChF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,MAAM,WAAW,wBAAwB;IACvC,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,GAAG,wBAAwB,CAqE3B"}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { readPersistedSessionPin, resolvePersistedPin, resolveSessionPin, writePersistedSessionPin, } from './initial-session-pin.js';
|
|
3
4
|
import { useOpenCodeSessions } from './use-opencode-sessions/index.js';
|
|
4
5
|
import { useProjectSession } from './use-project-session.js';
|
|
6
|
+
// SSR-safe: `localStorage` does not exist on the server, and reading it
|
|
7
|
+
// during render (rather than an effect) would make the client's FIRST
|
|
8
|
+
// hydration render disagree with the server-rendered HTML. Mirrors
|
|
9
|
+
// `useSession`'s own `useIsomorphicLayoutEffect` (T5) for the exact
|
|
10
|
+
// same reason — a `useLayoutEffect` commits its state update before the
|
|
11
|
+
// browser paints, so the corrected value is on screen for the user's very
|
|
12
|
+
// first frame instead of trailing one visible "empty" frame behind a plain
|
|
13
|
+
// `useEffect`.
|
|
14
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
5
15
|
/**
|
|
6
16
|
* OpenCode ↔ Kortix session mapping — READ side.
|
|
7
17
|
*
|
|
@@ -11,7 +21,12 @@ import { useProjectSession } from './use-project-session.js';
|
|
|
11
21
|
* the client (the old client-side `ensure-opencode` mutation caused the
|
|
12
22
|
* "session replaced / data lost" drift). It just surfaces the pin:
|
|
13
23
|
* 1. the value /start handed us this render (`pinFromStart`), else
|
|
14
|
-
* 2. the
|
|
24
|
+
* 2. the host's own warm-list pin (`initialPin`), else
|
|
25
|
+
* 3. the persisted row (`getProjectSession`) once its REST read resolves,
|
|
26
|
+
* or — before it does — this browser's own synchronous local mirror of
|
|
27
|
+
* the last canonical id seen for this session (T6). The local
|
|
28
|
+
* mirror exists ONLY to cover the gap before (3)'s network read lands;
|
|
29
|
+
* it never outranks it once loaded, and neither ever outranks (1).
|
|
15
30
|
*
|
|
16
31
|
* The OpenCode session list is still read (read-only) for ?oc deep-links and
|
|
17
32
|
* sidebar sub-session rendering.
|
|
@@ -44,12 +59,40 @@ export function useCanonicalOpenCodeSession(params) {
|
|
|
44
59
|
const projectSessionQuery = useProjectSession(projectId, sessionId, {
|
|
45
60
|
enabled: !pinFromStart && !initialPin,
|
|
46
61
|
});
|
|
47
|
-
const
|
|
62
|
+
const networkPin = projectSessionQuery.data?.opencode_session_id ?? null;
|
|
63
|
+
// T6 — a synchronous local mirror of the persisted pin, so a cold
|
|
64
|
+
// mount (no /start response yet, no host-warm `initialPin`) can still
|
|
65
|
+
// resolve `rootSessionId` on frame one instead of waiting out a network
|
|
66
|
+
// round trip. `useState(null)` matches what a server render sees (no
|
|
67
|
+
// `localStorage`), so hydration never disagrees with the server-rendered
|
|
68
|
+
// HTML; the layout effect below then corrects it to the real cached value
|
|
69
|
+
// before the browser paints — see the module-level comment on
|
|
70
|
+
// `useIsomorphicLayoutEffect`.
|
|
71
|
+
const [cachedPin, setCachedPin] = useState(null);
|
|
72
|
+
useIsomorphicLayoutEffect(() => {
|
|
73
|
+
setCachedPin(readPersistedSessionPin(projectId, sessionId));
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, [projectId, sessionId]);
|
|
76
|
+
const pin = resolvePersistedPin({ networkPin, cachedPin });
|
|
48
77
|
const rootSessionId = resolveSessionPin({
|
|
49
78
|
startPin: pinFromStart,
|
|
50
79
|
initialPin,
|
|
51
80
|
persistedPin: pin,
|
|
52
81
|
});
|
|
82
|
+
// Mirror a freshly-resolved canonical id back to disk so the NEXT mount of
|
|
83
|
+
// this (projectId, sessionId) can paint synchronously via `cachedPin`
|
|
84
|
+
// above. Scoped to the two AUTHORITATIVE, resolved-this-render sources —
|
|
85
|
+
// /start and the host's warm session-list pin — never to `pin` itself,
|
|
86
|
+
// which would just echo `cachedPin` back to its own key on every render
|
|
87
|
+
// this session is cold. This is also the write half of stale-pin
|
|
88
|
+
// convergence: a re-pinned session's fresh /start id overwrites whatever
|
|
89
|
+
// stale id a previous visit left behind, so the session AFTER this one
|
|
90
|
+
// reads the corrected value instead of the one that just got replaced.
|
|
91
|
+
const freshPin = pinFromStart ?? initialPin ?? null;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (freshPin)
|
|
94
|
+
writePersistedSessionPin(projectId, sessionId, freshPin);
|
|
95
|
+
}, [projectId, sessionId, freshPin]);
|
|
53
96
|
return {
|
|
54
97
|
rootSessionId,
|
|
55
98
|
sessions: sessionsQuery.data ?? [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-canonical-opencode-session.js","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"use-canonical-opencode-session.js","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ImpersonationSession } from '../core/http/impersonation.js';
|
|
2
|
+
/**
|
|
3
|
+
* React surface over the act-as store. The data logic (storage, expiry,
|
|
4
|
+
* header injection) lives in `core/http/impersonation`; these three hooks are
|
|
5
|
+
* the only React that knows about it, so the host renders a banner and a
|
|
6
|
+
* button and nothing else.
|
|
7
|
+
*/
|
|
8
|
+
/** Wire path of the mint route. */
|
|
9
|
+
export declare const ADMIN_IMPERSONATE_PATH = "/admin/api/impersonate";
|
|
10
|
+
/** Wire path of the revoke route for one grant. */
|
|
11
|
+
export declare function adminImpersonateRevokePath(grantId: string): string;
|
|
12
|
+
/** Wire path of the caller's live grants. */
|
|
13
|
+
export declare const ADMIN_IMPERSONATE_ACTIVE_PATH = "/admin/api/impersonate/active";
|
|
14
|
+
/** What `POST /admin/api/impersonate` answers. */
|
|
15
|
+
export interface AdminImpersonateResponse {
|
|
16
|
+
grant_id: string;
|
|
17
|
+
account_id: string;
|
|
18
|
+
account_name?: string | null;
|
|
19
|
+
/** ISO-8601. Server-capped at one hour — the client never chooses it. */
|
|
20
|
+
expires_at: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The current act-as session, or null. Subscribes to the store, so a banner
|
|
24
|
+
* built on it disappears by itself when the grant expires or is exited from
|
|
25
|
+
* another component.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useImpersonation(): ImpersonationSession | null;
|
|
28
|
+
/**
|
|
29
|
+
* Start acting as an account. On success the session is stored, which makes
|
|
30
|
+
* every subsequent request from this tab carry the grant header — so the
|
|
31
|
+
* caller navigates to the app shell and nothing else needs wiring.
|
|
32
|
+
*
|
|
33
|
+
* The whole React Query cache is cleared, not invalidated: every cached entry
|
|
34
|
+
* was fetched as the OPERATOR, and re-serving one inside a customer's session
|
|
35
|
+
* would show the operator's own data under the customer's banner.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useAdminImpersonate(): import("@tanstack/react-query").UseMutationResult<AdminImpersonateResponse, Error, {
|
|
38
|
+
accountId: string;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Stop acting. Revokes server-side FIRST, then clears the local session — in
|
|
43
|
+
* that order, because a client that forgot the grant id can no longer revoke
|
|
44
|
+
* it, and the row would stay live for the rest of its hour.
|
|
45
|
+
*
|
|
46
|
+
* A failed revoke still clears locally: the operator asked to leave, and the
|
|
47
|
+
* grant expires on its own. The error is surfaced so the caller can say so.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useStopImpersonation(): import("@tanstack/react-query").UseMutationResult<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
} | null, Error, void | {
|
|
52
|
+
grantId?: string;
|
|
53
|
+
}, unknown>;
|
|
54
|
+
//# sourceMappingURL=use-impersonation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-impersonation.d.ts","sourceRoot":"","sources":["../../src/react/use-impersonation.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAEpC;;;;;GAKG;AAEH,mCAAmC;AACnC,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D,mDAAmD;AACnD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,6CAA6C;AAC7C,eAAO,MAAM,6BAA6B,kCAAqC,CAAC;AAEhF,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,oBAAoB,GAAG,IAAI,CAS9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB;eAEgC,MAAM;aAAW,MAAM;YAqBzF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB;QAET,OAAO;;cAA8B,MAAM;YAgBrE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { backendApi } from '../core/http/api-client.js';
|
|
4
|
+
import { clearImpersonationSession, getImpersonationSession, setImpersonationSession, subscribeToImpersonation, } from '../core/http/impersonation.js';
|
|
5
|
+
/**
|
|
6
|
+
* React surface over the act-as store. The data logic (storage, expiry,
|
|
7
|
+
* header injection) lives in `core/http/impersonation`; these three hooks are
|
|
8
|
+
* the only React that knows about it, so the host renders a banner and a
|
|
9
|
+
* button and nothing else.
|
|
10
|
+
*/
|
|
11
|
+
/** Wire path of the mint route. */
|
|
12
|
+
export const ADMIN_IMPERSONATE_PATH = '/admin/api/impersonate';
|
|
13
|
+
/** Wire path of the revoke route for one grant. */
|
|
14
|
+
export function adminImpersonateRevokePath(grantId) {
|
|
15
|
+
return `${ADMIN_IMPERSONATE_PATH}/${encodeURIComponent(grantId)}`;
|
|
16
|
+
}
|
|
17
|
+
/** Wire path of the caller's live grants. */
|
|
18
|
+
export const ADMIN_IMPERSONATE_ACTIVE_PATH = `${ADMIN_IMPERSONATE_PATH}/active`;
|
|
19
|
+
/**
|
|
20
|
+
* The current act-as session, or null. Subscribes to the store, so a banner
|
|
21
|
+
* built on it disappears by itself when the grant expires or is exited from
|
|
22
|
+
* another component.
|
|
23
|
+
*/
|
|
24
|
+
export function useImpersonation() {
|
|
25
|
+
return useSyncExternalStore(subscribeToImpersonation, getImpersonationSession,
|
|
26
|
+
// Server render: never acting as an account. The store is per-tab
|
|
27
|
+
// `sessionStorage`, which does not exist during SSR, and claiming a session
|
|
28
|
+
// there would flash a banner for an operator who has none.
|
|
29
|
+
() => null);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start acting as an account. On success the session is stored, which makes
|
|
33
|
+
* every subsequent request from this tab carry the grant header — so the
|
|
34
|
+
* caller navigates to the app shell and nothing else needs wiring.
|
|
35
|
+
*
|
|
36
|
+
* The whole React Query cache is cleared, not invalidated: every cached entry
|
|
37
|
+
* was fetched as the OPERATOR, and re-serving one inside a customer's session
|
|
38
|
+
* would show the operator's own data under the customer's banner.
|
|
39
|
+
*/
|
|
40
|
+
export function useAdminImpersonate() {
|
|
41
|
+
const queryClient = useQueryClient();
|
|
42
|
+
return useMutation({
|
|
43
|
+
mutationFn: async ({ accountId, reason }) => {
|
|
44
|
+
const body = { account_id: accountId };
|
|
45
|
+
if (reason)
|
|
46
|
+
body.reason = reason;
|
|
47
|
+
const response = await backendApi.post(ADMIN_IMPERSONATE_PATH, body);
|
|
48
|
+
if (response.error)
|
|
49
|
+
throw new Error(response.error.message);
|
|
50
|
+
return response.data;
|
|
51
|
+
},
|
|
52
|
+
onSuccess: (data) => {
|
|
53
|
+
setImpersonationSession({
|
|
54
|
+
grantId: data.grant_id,
|
|
55
|
+
accountId: data.account_id,
|
|
56
|
+
accountName: data.account_name ?? null,
|
|
57
|
+
expiresAt: data.expires_at,
|
|
58
|
+
});
|
|
59
|
+
queryClient.clear();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Stop acting. Revokes server-side FIRST, then clears the local session — in
|
|
65
|
+
* that order, because a client that forgot the grant id can no longer revoke
|
|
66
|
+
* it, and the row would stay live for the rest of its hour.
|
|
67
|
+
*
|
|
68
|
+
* A failed revoke still clears locally: the operator asked to leave, and the
|
|
69
|
+
* grant expires on its own. The error is surfaced so the caller can say so.
|
|
70
|
+
*/
|
|
71
|
+
export function useStopImpersonation() {
|
|
72
|
+
const queryClient = useQueryClient();
|
|
73
|
+
return useMutation({
|
|
74
|
+
mutationFn: async (variables) => {
|
|
75
|
+
const grantId = variables?.grantId ?? getImpersonationSession()?.grantId;
|
|
76
|
+
if (!grantId)
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const response = await backendApi.delete(adminImpersonateRevokePath(grantId));
|
|
80
|
+
if (response.error)
|
|
81
|
+
throw new Error(response.error.message);
|
|
82
|
+
return response.data ?? { ok: true };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearImpersonationSession();
|
|
86
|
+
queryClient.clear();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=use-impersonation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-impersonation.js","sourceRoot":"","sources":["../../src/react/use-impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,GAEzB,MAAM,4BAA4B,CAAC;AAEpC;;;;;GAKG;AAEH,mCAAmC;AACnC,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D,mDAAmD;AACnD,MAAM,UAAU,0BAA0B,CAAC,OAAe;IACxD,OAAO,GAAG,sBAAsB,IAAI,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,6CAA6C;AAC7C,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,sBAAsB,SAAS,CAAC;AAWhF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,oBAAoB,CACzB,wBAAwB,EACxB,uBAAuB;IACvB,kEAAkE;IAClE,4EAA4E;IAC5E,2DAA2D;IAC3D,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,OAAO,WAAW,CAA0E;QAC1F,UAAU,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE;YAC1C,MAAM,IAAI,GAA4B,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;YAChE,IAAI,MAAM;gBAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACjC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CACpC,sBAAsB,EACtB,IAAI,CACL,CAAC;YACF,IAAI,QAAQ,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5D,OAAO,QAAQ,CAAC,IAAK,CAAC;QACxB,CAAC;QACD,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;YAClB,uBAAuB,CAAC;gBACtB,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,SAAS,EAAE,IAAI,CAAC,UAAU;gBAC1B,WAAW,EAAE,IAAI,CAAC,YAAY,IAAI,IAAI;gBACtC,SAAS,EAAE,IAAI,CAAC,UAAU;aAC3B,CAAC,CAAC;YACH,WAAW,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,OAAO,WAAW,CAA6D;QAC7E,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;YAC9B,MAAM,OAAO,GAAG,SAAS,EAAE,OAAO,IAAI,uBAAuB,EAAE,EAAE,OAAO,CAAC;YACzE,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CACtC,0BAA0B,CAAC,OAAO,CAAC,CACpC,CAAC;gBACF,IAAI,QAAQ,CAAC,KAAK;oBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC5D,OAAO,QAAQ,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;YACvC,CAAC;oBAAS,CAAC;gBACT,yBAAyB,EAAE,CAAC;gBAC5B,WAAW,CAAC,KAAK,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -4,6 +4,8 @@ import type { RefObject } from 'react';
|
|
|
4
4
|
import { getClient } from '../../core/runtime/client.js';
|
|
5
5
|
import { type SessionSyncReason } from '../../core/session-sync/session-sync-controller.js';
|
|
6
6
|
import type { NormalizeDiagnosticPaths, OpenCodeEvent } from './types.js';
|
|
7
|
+
/** See `createEventHandler`'s `userPartsGraceMs`. */
|
|
8
|
+
export declare const USER_PARTS_GRACE_MS = 1500;
|
|
7
9
|
/** Builds the SSE event handler; all closure dependencies are injected. */
|
|
8
10
|
export declare function createEventHandler(deps: {
|
|
9
11
|
queryClient: QueryClient;
|
|
@@ -18,6 +20,14 @@ export declare function createEventHandler(deps: {
|
|
|
18
20
|
markSessionAbortedLocally: RefObject<(sessionID: string, message?: string) => void>;
|
|
19
21
|
fetchLspDiagnosticsDebounced: RefObject<() => void>;
|
|
20
22
|
reconcileSessionTail?: (sessionID: string, reason: SessionSyncReason) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* How long a USER `message.updated` may sit with no parts before the tail is
|
|
25
|
+
* re-read. The runtime emits the info frame and the text part separately;
|
|
26
|
+
* lose the part (a stream reconnect during the boot hand-off) and the
|
|
27
|
+
* transcript shows an empty bubble until a reload. Injected so tests can
|
|
28
|
+
* make it immediate.
|
|
29
|
+
*/
|
|
30
|
+
userPartsGraceMs?: number;
|
|
21
31
|
/** The route-scoped project this SSE connection belongs to — see
|
|
22
32
|
* `refetchKortixSessionMirrors`'s doc comment for why this can't default
|
|
23
33
|
* to "every project". Optional only so existing test harnesses that don't
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handle-event.d.ts","sourceRoot":"","sources":["../../../src/react/use-opencode-events/handle-event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EACV,KAAK,IAAI,gBAAgB,EACzB,iBAAiB,EACjB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"handle-event.d.ts","sourceRoot":"","sources":["../../../src/react/use-opencode-events/handle-event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EACV,KAAK,IAAI,gBAAgB,EACzB,iBAAiB,EACjB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,iDAAiD,CAAC;AAazD,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvE,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;IACrC,cAAc,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,aAAa,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,wBAAwB,EAAE,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC9D,yBAAyB,EAAE,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IACpF,4BAA4B,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;IACpD,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;6DAIyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,WAqC6B,aAAa,UA0jB1C"}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { infoToast, notifyPermissionRequest, notifyQuestion, notifySessionError, notifyTaskComplete, } from '../../platform/ui.js';
|
|
2
2
|
import { deleteSessionFromIDB } from '../../browser/cache/idb-sync-cache.js';
|
|
3
3
|
import { useSyncStore } from '../../browser/stores/sync-store.js';
|
|
4
|
+
import { isAbortError } from '../../core/http/abort-error.js';
|
|
4
5
|
import { getClient } from '../../core/runtime/client.js';
|
|
5
6
|
import { SESSION_SYNC_PAGE_SIZE, } from '../../core/session-sync/session-sync-controller.js';
|
|
6
7
|
import { fileContentKeys, fileListKeys, gitStatusKeys } from '../file-keys.js';
|
|
7
8
|
import { ptyKeys } from '../use-opencode-pty.js';
|
|
8
9
|
import { opencodeKeys } from '../use-opencode-sessions/index.js';
|
|
9
10
|
import { applyPartDiagnostics } from './diagnostics.js';
|
|
10
|
-
import { asStringOrUndefined,
|
|
11
|
+
import { asStringOrUndefined, patchKortixSessionTitleMirrors, readSessionInfo, realRuntimeTitle, refetchKortixSessionMirrors, scheduleProjectMetadataRefetch, } from './helpers.js';
|
|
12
|
+
/** See `createEventHandler`'s `userPartsGraceMs`. */
|
|
13
|
+
export const USER_PARTS_GRACE_MS = 1_500;
|
|
11
14
|
/** Builds the SSE event handler; all closure dependencies are injected. */
|
|
12
15
|
export function createEventHandler(deps) {
|
|
13
|
-
const { queryClient, client, applySyncEvent, stopCompaction, addPermission, removePermission, addQuestion, removeQuestion, normalizeDiagnosticPaths, markSessionAbortedLocally, fetchLspDiagnosticsDebounced, projectId = null, } = deps;
|
|
16
|
+
const { queryClient, client, applySyncEvent, stopCompaction, addPermission, removePermission, addQuestion, removeQuestion, normalizeDiagnosticPaths, markSessionAbortedLocally, fetchLspDiagnosticsDebounced, projectId = null, userPartsGraceMs = USER_PARTS_GRACE_MS, } = deps;
|
|
14
17
|
const reconcileTail = deps.reconcileSessionTail ??
|
|
15
18
|
(async (sessionID) => {
|
|
16
19
|
const result = await client.session.messages({
|
|
@@ -43,7 +46,24 @@ export function createEventHandler(deps) {
|
|
|
43
46
|
applySyncEvent(event);
|
|
44
47
|
switch (event.type) {
|
|
45
48
|
// ---- Message events — handled by sync store only ----
|
|
46
|
-
case 'message.updated':
|
|
49
|
+
case 'message.updated': {
|
|
50
|
+
// A user message is two frames — the info, then its text part. If the
|
|
51
|
+
// second never lands this tab renders an empty bubble for good; a
|
|
52
|
+
// tail re-read after a short grace repairs it from the server.
|
|
53
|
+
const info = event.properties.info;
|
|
54
|
+
const sessionID = info?.sessionID ?? event.properties.sessionID;
|
|
55
|
+
if (info?.role === 'user' && info.id && sessionID) {
|
|
56
|
+
const messageID = info.id;
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
const state = useSyncStore.getState();
|
|
59
|
+
const stillListed = state.messages[sessionID]?.some((m) => m.id === messageID);
|
|
60
|
+
if (stillListed && !(state.parts[messageID]?.length ?? 0)) {
|
|
61
|
+
void reconcileTail(sessionID, 'sse-gap');
|
|
62
|
+
}
|
|
63
|
+
}, userPartsGraceMs);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
47
67
|
case 'message.removed':
|
|
48
68
|
break;
|
|
49
69
|
case 'message.part.updated': {
|
|
@@ -62,6 +82,15 @@ export function createEventHandler(deps) {
|
|
|
62
82
|
case 'session.created': {
|
|
63
83
|
const info = readSessionInfo(event);
|
|
64
84
|
if (info) {
|
|
85
|
+
// T22 — reload/cross-tab recovery: a `Session.revert` field on the
|
|
86
|
+
// info this event carries is the only way a fresh mount (nothing
|
|
87
|
+
// staged locally yet, the `.staged` wire event already happened
|
|
88
|
+
// before this tab connected) rediscovers an already-staged revert.
|
|
89
|
+
// See `sync-store.ts`'s `syncSessionRevertFromInfo` doc comment —
|
|
90
|
+
// absence never clears an existing record; only the three
|
|
91
|
+
// dedicated wire events (also routed through `applySyncEvent`
|
|
92
|
+
// above) do that.
|
|
93
|
+
useSyncStore.getState().syncSessionRevertFromInfo(info.id, info.revert ?? null);
|
|
65
94
|
queryClient.setQueryData(opencodeKeys.sessions(), (old) => {
|
|
66
95
|
if (!old)
|
|
67
96
|
return [info];
|
|
@@ -85,6 +114,8 @@ export function createEventHandler(deps) {
|
|
|
85
114
|
case 'session.updated': {
|
|
86
115
|
const info = readSessionInfo(event);
|
|
87
116
|
if (info) {
|
|
117
|
+
// T22 — see the identical call in the `session.created` case above.
|
|
118
|
+
useSyncStore.getState().syncSessionRevertFromInfo(info.id, info.revert ?? null);
|
|
88
119
|
// OpenCode auto-titles after the first message via session.updated.
|
|
89
120
|
// Capture the previous title before local cache mutation so we only
|
|
90
121
|
// force the server-owned mirror read when the title actually changed.
|
|
@@ -150,7 +181,21 @@ export function createEventHandler(deps) {
|
|
|
150
181
|
const client = getClient();
|
|
151
182
|
void reconcileTail(sessionID, 'compaction');
|
|
152
183
|
// Refetch the individual session to clear time.compacting
|
|
153
|
-
// (targeted refetch, not full session list invalidation)
|
|
184
|
+
// (targeted refetch, not full session list invalidation). This is
|
|
185
|
+
// the FAST path — it fires the instant the frame arrives, and on
|
|
186
|
+
// success also patches the session list mirror.
|
|
187
|
+
//
|
|
188
|
+
// It is not the ONLY path any more: a failure here (this fetch has
|
|
189
|
+
// no retry of its own) used to leave `time.compacting` stale in the
|
|
190
|
+
// `opencodeKeys.runtimeSession` cache FOREVER — `useOpenCodeSession`
|
|
191
|
+
// reads it with `staleTime: Infinity` and nothing else refetches it,
|
|
192
|
+
// so `projectCompacting`'s server-observed rule (`core/session/
|
|
193
|
+
// compaction.ts`) stayed pinned `true` for the rest of the tab's
|
|
194
|
+
// life. On failure, route the retry through the SAME query
|
|
195
|
+
// `useOpenCodeSession` registers (3 attempts, exponential backoff)
|
|
196
|
+
// instead of swallowing it silently. `use-session.ts` also arms a
|
|
197
|
+
// `serverCompactionRevalidateAtMs` timer as a second, frame-
|
|
198
|
+
// independent backstop — see that function's doc comment.
|
|
154
199
|
client.session
|
|
155
200
|
.get({ sessionID })
|
|
156
201
|
.then((res) => {
|
|
@@ -169,8 +214,32 @@ export function createEventHandler(deps) {
|
|
|
169
214
|
return next;
|
|
170
215
|
});
|
|
171
216
|
}
|
|
217
|
+
else {
|
|
218
|
+
void queryClient.invalidateQueries({
|
|
219
|
+
queryKey: opencodeKeys.runtimeSession(sessionID),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
172
222
|
})
|
|
173
|
-
.catch(() => {
|
|
223
|
+
.catch(() => {
|
|
224
|
+
void queryClient.invalidateQueries({
|
|
225
|
+
queryKey: opencodeKeys.runtimeSession(sessionID),
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
// ---- Rewind commit in a view that never saw the staging ----
|
|
232
|
+
case 'session.next.revert.committed': {
|
|
233
|
+
// The store's `applyEvent` refuses to guess-delete when this tab has
|
|
234
|
+
// no tracked revert record (second tab / fresh mount) — guessing a
|
|
235
|
+
// watermark from the local tip can delete the user's REPLACEMENT
|
|
236
|
+
// prompt. It raises `sessionRevertNeedsTailReconcile` instead; this
|
|
237
|
+
// is the one consumer: fetch the server's already-truncated tail so
|
|
238
|
+
// truth arrives by read, not by guess.
|
|
239
|
+
const { sessionID } = event.properties;
|
|
240
|
+
if (sessionID && useSyncStore.getState().sessionRevertNeedsTailReconcile[sessionID]) {
|
|
241
|
+
useSyncStore.getState().clearSessionRevertNeedsTailReconcile(sessionID);
|
|
242
|
+
void reconcileTail(sessionID, 'manual');
|
|
174
243
|
}
|
|
175
244
|
break;
|
|
176
245
|
}
|
|
@@ -270,8 +339,8 @@ export function createEventHandler(deps) {
|
|
|
270
339
|
// may not have persisted the partial assistant response yet,
|
|
271
340
|
// so hydrating would wipe the streamed content the user saw.
|
|
272
341
|
// The error is already patched onto the message above.
|
|
273
|
-
const
|
|
274
|
-
if (!
|
|
342
|
+
const aborted = isAbortError(error);
|
|
343
|
+
if (!aborted) {
|
|
275
344
|
reconcileTail(sessionID, 'session-error')
|
|
276
345
|
.then(() => {
|
|
277
346
|
useSyncStore.getState().clearOptimisticMessages(sessionID);
|