@opengeni/db 0.28.9 → 0.36.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/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +299 -0
- package/dist/browser-downloads.d.ts +51 -0
- package/dist/browser-identities.d.ts +137 -0
- package/dist/browser-sessions.d.ts +354 -0
- package/dist/browser-state-artifacts.d.ts +91 -0
- package/dist/canonical-human-identities.d.ts +48 -0
- package/dist/canonical-human-identities.js +28 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +194 -0
- package/dist/chunk-5KTIEDX7.js +13200 -0
- package/dist/chunk-5KTIEDX7.js.map +1 -0
- package/dist/chunk-CQZ4QSP5.js +995 -0
- package/dist/chunk-CQZ4QSP5.js.map +1 -0
- package/dist/chunk-F32YT7MP.js +117 -0
- package/dist/chunk-F32YT7MP.js.map +1 -0
- package/dist/chunk-JC5QPLUM.js +666 -0
- package/dist/chunk-JC5QPLUM.js.map +1 -0
- package/dist/chunk-P3RE4D2B.js +3535 -0
- package/dist/chunk-P3RE4D2B.js.map +1 -0
- package/dist/chunk-TAUTWOS5.js +259 -0
- package/dist/chunk-TAUTWOS5.js.map +1 -0
- package/dist/chunk-VKLB2ZGI.js +2389 -0
- package/dist/chunk-VKLB2ZGI.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +57 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +1122 -126
- package/dist/index.js +42329 -18407
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +73 -10
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-facets.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +14 -0
- package/dist/interaction-schema.d.ts +6036 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +3 -3
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +19 -6
- package/dist/schema.d.ts +22654 -9359
- package/dist/schema.js +209 -3
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +33 -7
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tenancy.d.ts +44 -0
- package/dist/session-tenancy.js +16 -0
- package/dist/session-tenancy.js.map +1 -0
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-task-policy-schema.d.ts +847 -0
- package/dist/slack-task-policy.d.ts +65 -0
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/task-notes-schema.d.ts +924 -0
- package/dist/task-notes.d.ts +85 -0
- package/dist/transcription-recordings.d.ts +2 -2
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +225 -0
- package/dist/video-generation.js +68 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/dist/xai-subscription.d.ts +326 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_browser_interaction_authority.sql +67 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_browser_download_saves.sql +34 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_browser_controller_host.sql +385 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/drizzle/0217_external_browser_auth_operations.sql +22 -0
- package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
- package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
- package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
- package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
- package/drizzle/0220_session_channels.sql +54 -0
- package/drizzle/0221_sessions_channel_index.sql +5 -0
- package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
- package/drizzle/0222_sessions_channel_fk.sql +89 -0
- package/drizzle/0223_pending_tool_event_output.sql +55 -0
- package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
- package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
- package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
- package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
- package/drizzle/0227_slack_native_actions.sql +145 -0
- package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
- package/drizzle/0228_slack_task_policy.sql +400 -0
- package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
- package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
- package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
- package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
- package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
- package/drizzle/0234_xai_subscription_authority.sql +1139 -0
- package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
- package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
- package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
- package/drizzle/0237_interaction_transition_reaper.sql +263 -0
- package/drizzle/0238_goal_persistence_policy.sql +471 -0
- package/drizzle/0238_supergrok_realtime_model.sql +29 -0
- package/drizzle/0239_supergrok_video_funding.sql +50 -0
- package/drizzle/0239_task_tree_notes.sql +990 -0
- package/package.json +26 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +4373 -0
- package/src/browser-downloads.ts +517 -0
- package/src/browser-identities.ts +1385 -0
- package/src/browser-sessions.ts +2294 -0
- package/src/browser-state-artifacts.ts +449 -0
- package/src/canonical-human-identities.ts +317 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1571 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +468 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/generated-images.ts +740 -0
- package/src/index.ts +9963 -1465
- package/src/insights.ts +251 -23
- package/src/integration-bindings.ts +455 -0
- package/src/integration-facets.ts +862 -0
- package/src/interaction-revisions.ts +70 -0
- package/src/interaction-schema.ts +1606 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +886 -0
- package/src/persistence-errors.ts +4 -7
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +534 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +721 -5
- package/src/schema.ts +6270 -3159
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +130 -42
- package/src/session-queue-commands.ts +265 -15
- package/src/session-realtime-context.ts +2 -2
- package/src/session-realtime-ledger.ts +3 -3
- package/src/session-realtime.ts +7 -7
- package/src/session-tenancy.ts +188 -0
- package/src/session-tool-call-settlement.ts +18 -3
- package/src/slack-task-policy-schema.ts +130 -0
- package/src/slack-task-policy.ts +341 -0
- package/src/slack-user-link-access.ts +702 -0
- package/src/task-notes-schema.ts +135 -0
- package/src/task-notes.ts +178 -0
- package/src/video-generation.ts +1530 -0
- package/src/workspace-artifacts.ts +1 -1
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/src/xai-subscription.ts +1887 -0
- package/dist/chunk-P6CUHXQZ.js +0 -7248
- package/dist/chunk-P6CUHXQZ.js.map +0 -1
- package/dist/chunk-VHBFHMPC.js +0 -1219
- package/dist/chunk-VHBFHMPC.js.map +0 -1
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Permission,
|
|
5
|
+
SlackUserLinkAccessRequest as SlackUserLinkAccessRequestSchema,
|
|
6
|
+
type SlackUserLinkAccessRequest,
|
|
7
|
+
type SlackUserLinkAccessRequestStatus,
|
|
8
|
+
} from "@opengeni/contracts";
|
|
9
|
+
import { and, asc, eq, gt, inArray, ne } from "drizzle-orm";
|
|
10
|
+
|
|
11
|
+
import { type Database, withWorkspaceRls } from "./database";
|
|
12
|
+
import { withLosslessContentWriteVersion } from "./lossless-json";
|
|
13
|
+
import * as schema from "./schema";
|
|
14
|
+
|
|
15
|
+
type RequestRow = typeof schema.slackUserLinkAccessRequests.$inferSelect;
|
|
16
|
+
type OperationKind = "request" | "cancel" | "approve" | "deny";
|
|
17
|
+
|
|
18
|
+
export class SlackUserLinkAccessPersistenceError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly code:
|
|
21
|
+
| "not_found"
|
|
22
|
+
| "subject_mismatch"
|
|
23
|
+
| "claim_mismatch"
|
|
24
|
+
| "version_conflict"
|
|
25
|
+
| "state_conflict"
|
|
26
|
+
| "idempotency_conflict"
|
|
27
|
+
| "slack_identity_conflict",
|
|
28
|
+
) {
|
|
29
|
+
super(code);
|
|
30
|
+
this.name = "SlackUserLinkAccessPersistenceError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PrepareSlackUserLinkAccessInput = {
|
|
35
|
+
accountId: string;
|
|
36
|
+
workspaceId: string;
|
|
37
|
+
tokenDigest: string;
|
|
38
|
+
connectionId: string;
|
|
39
|
+
slackTeamId: string;
|
|
40
|
+
slackUserId: string;
|
|
41
|
+
subjectId: string;
|
|
42
|
+
subjectLabel?: string | null;
|
|
43
|
+
expiresAt: Date;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type SlackUserLinkAccessMutationInput = {
|
|
47
|
+
workspaceId: string;
|
|
48
|
+
requestId: string;
|
|
49
|
+
actorSubjectId: string;
|
|
50
|
+
expectedVersion: number;
|
|
51
|
+
idempotencyKey: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ApproveSlackUserLinkAccessInput = SlackUserLinkAccessMutationInput & {
|
|
55
|
+
role?: string;
|
|
56
|
+
permissions: Permission[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const activeStatuses: SlackUserLinkAccessRequestStatus[] = ["prepared", "pending"];
|
|
60
|
+
|
|
61
|
+
function isActiveStatus(status: string): status is (typeof activeStatuses)[number] {
|
|
62
|
+
return activeStatuses.includes(status as (typeof activeStatuses)[number]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Persist one verified Slack bearer as token-free, subject-bound state. A
|
|
67
|
+
* second fresh bearer for the same exact subject/provider principal supersedes
|
|
68
|
+
* only that subject's older active intent; another subject's row is untouched.
|
|
69
|
+
*/
|
|
70
|
+
export async function prepareSlackUserLinkAccessRequest(
|
|
71
|
+
db: Database,
|
|
72
|
+
input: PrepareSlackUserLinkAccessInput,
|
|
73
|
+
now = new Date(),
|
|
74
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
75
|
+
return await withWorkspaceRls(
|
|
76
|
+
db,
|
|
77
|
+
input.workspaceId,
|
|
78
|
+
async (scopedDb) =>
|
|
79
|
+
await scopedDb.transaction(async (tx) => {
|
|
80
|
+
const database = tx as unknown as Database;
|
|
81
|
+
const [existing] = await tx
|
|
82
|
+
.select()
|
|
83
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
84
|
+
.where(eq(schema.slackUserLinkAccessRequests.tokenDigest, input.tokenDigest))
|
|
85
|
+
.for("update")
|
|
86
|
+
.limit(1);
|
|
87
|
+
if (existing) {
|
|
88
|
+
assertExactClaims(existing, input);
|
|
89
|
+
if (existing.subjectId !== input.subjectId) {
|
|
90
|
+
throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
|
|
91
|
+
}
|
|
92
|
+
return mapRequest(await expireLockedRequest(database, existing, now));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const superseded = await tx
|
|
96
|
+
.select()
|
|
97
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
98
|
+
.where(
|
|
99
|
+
and(
|
|
100
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
|
|
101
|
+
eq(schema.slackUserLinkAccessRequests.connectionId, input.connectionId),
|
|
102
|
+
eq(schema.slackUserLinkAccessRequests.slackUserId, input.slackUserId),
|
|
103
|
+
eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
|
|
104
|
+
inArray(schema.slackUserLinkAccessRequests.status, activeStatuses),
|
|
105
|
+
ne(schema.slackUserLinkAccessRequests.tokenDigest, input.tokenDigest),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
.for("update");
|
|
109
|
+
for (const row of superseded) {
|
|
110
|
+
const current = await expireLockedRequest(database, row, now);
|
|
111
|
+
if (!isActiveStatus(current.status)) continue;
|
|
112
|
+
const [cancelled] = await tx
|
|
113
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
114
|
+
.set({
|
|
115
|
+
status: "cancelled",
|
|
116
|
+
version: current.version + 1,
|
|
117
|
+
decidedAt: now,
|
|
118
|
+
decisionBySubjectId: input.subjectId,
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
})
|
|
121
|
+
.where(
|
|
122
|
+
and(
|
|
123
|
+
eq(schema.slackUserLinkAccessRequests.id, current.id),
|
|
124
|
+
eq(schema.slackUserLinkAccessRequests.version, current.version),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
.returning();
|
|
128
|
+
if (cancelled) {
|
|
129
|
+
await insertAudit(database, cancelled, input.subjectId, "slack.user_link.superseded");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const [created] = await tx
|
|
134
|
+
.insert(schema.slackUserLinkAccessRequests)
|
|
135
|
+
.values({
|
|
136
|
+
accountId: input.accountId,
|
|
137
|
+
workspaceId: input.workspaceId,
|
|
138
|
+
tokenDigest: input.tokenDigest,
|
|
139
|
+
connectionId: input.connectionId,
|
|
140
|
+
slackTeamId: input.slackTeamId,
|
|
141
|
+
slackUserId: input.slackUserId,
|
|
142
|
+
subjectId: input.subjectId,
|
|
143
|
+
subjectLabel: input.subjectLabel ?? null,
|
|
144
|
+
expiresAt: input.expiresAt,
|
|
145
|
+
})
|
|
146
|
+
.returning();
|
|
147
|
+
if (!created) throw new Error("Slack user-link access request insert returned no row");
|
|
148
|
+
await insertAudit(database, created, input.subjectId, "slack.user_link.prepared");
|
|
149
|
+
return mapRequest(created);
|
|
150
|
+
}),
|
|
151
|
+
{ isolationLevel: "serializable" },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function getSlackUserLinkAccessRequestForSubject(
|
|
156
|
+
db: Database,
|
|
157
|
+
input: { workspaceId: string; requestId: string; subjectId: string },
|
|
158
|
+
now = new Date(),
|
|
159
|
+
): Promise<SlackUserLinkAccessRequest | null> {
|
|
160
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
161
|
+
return await scopedDb.transaction(async (tx) => {
|
|
162
|
+
const [row] = await tx
|
|
163
|
+
.select()
|
|
164
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
165
|
+
.where(
|
|
166
|
+
and(
|
|
167
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
|
|
168
|
+
eq(schema.slackUserLinkAccessRequests.id, input.requestId),
|
|
169
|
+
eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
.for("update")
|
|
173
|
+
.limit(1);
|
|
174
|
+
if (!row) return null;
|
|
175
|
+
return mapRequest(await expireLockedRequest(tx as unknown as Database, row, now));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function requestSlackUserLinkWorkspaceAccess(
|
|
181
|
+
db: Database,
|
|
182
|
+
input: SlackUserLinkAccessMutationInput,
|
|
183
|
+
now = new Date(),
|
|
184
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
185
|
+
return await mutateRequest(db, input, "request", {}, now, async (database, row) => {
|
|
186
|
+
if (row.subjectId !== input.actorSubjectId) {
|
|
187
|
+
throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
|
|
188
|
+
}
|
|
189
|
+
if (row.status !== "prepared") {
|
|
190
|
+
throw new SlackUserLinkAccessPersistenceError("state_conflict");
|
|
191
|
+
}
|
|
192
|
+
const [updated] = await database
|
|
193
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
194
|
+
.set({
|
|
195
|
+
status: "pending",
|
|
196
|
+
version: row.version + 1,
|
|
197
|
+
requestedAt: now,
|
|
198
|
+
updatedAt: now,
|
|
199
|
+
})
|
|
200
|
+
.where(
|
|
201
|
+
and(
|
|
202
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
203
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
.returning();
|
|
207
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
208
|
+
await insertAudit(database, updated, input.actorSubjectId, "workspace.access_request.created");
|
|
209
|
+
return updated;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function cancelSlackUserLinkAccessRequest(
|
|
214
|
+
db: Database,
|
|
215
|
+
input: SlackUserLinkAccessMutationInput,
|
|
216
|
+
now = new Date(),
|
|
217
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
218
|
+
return await mutateRequest(db, input, "cancel", {}, now, async (database, row) => {
|
|
219
|
+
if (row.subjectId !== input.actorSubjectId) {
|
|
220
|
+
throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
|
|
221
|
+
}
|
|
222
|
+
if (row.status !== "prepared" && row.status !== "pending") {
|
|
223
|
+
throw new SlackUserLinkAccessPersistenceError("state_conflict");
|
|
224
|
+
}
|
|
225
|
+
const [updated] = await database
|
|
226
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
227
|
+
.set({
|
|
228
|
+
status: "cancelled",
|
|
229
|
+
version: row.version + 1,
|
|
230
|
+
decidedAt: now,
|
|
231
|
+
decisionBySubjectId: input.actorSubjectId,
|
|
232
|
+
updatedAt: now,
|
|
233
|
+
})
|
|
234
|
+
.where(
|
|
235
|
+
and(
|
|
236
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
237
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
.returning();
|
|
241
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
242
|
+
await insertAudit(
|
|
243
|
+
database,
|
|
244
|
+
updated,
|
|
245
|
+
input.actorSubjectId,
|
|
246
|
+
"workspace.access_request.cancelled",
|
|
247
|
+
);
|
|
248
|
+
return updated;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function denySlackUserLinkAccessRequest(
|
|
253
|
+
db: Database,
|
|
254
|
+
input: SlackUserLinkAccessMutationInput,
|
|
255
|
+
now = new Date(),
|
|
256
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
257
|
+
return await mutateRequest(db, input, "deny", {}, now, async (database, row) => {
|
|
258
|
+
if (row.status !== "pending") {
|
|
259
|
+
throw new SlackUserLinkAccessPersistenceError("state_conflict");
|
|
260
|
+
}
|
|
261
|
+
const [updated] = await database
|
|
262
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
263
|
+
.set({
|
|
264
|
+
status: "denied",
|
|
265
|
+
version: row.version + 1,
|
|
266
|
+
decidedAt: now,
|
|
267
|
+
decisionBySubjectId: input.actorSubjectId,
|
|
268
|
+
updatedAt: now,
|
|
269
|
+
})
|
|
270
|
+
.where(
|
|
271
|
+
and(
|
|
272
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
273
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
.returning();
|
|
277
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
278
|
+
await insertAudit(database, updated, input.actorSubjectId, "workspace.access_request.denied");
|
|
279
|
+
return updated;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function approveSlackUserLinkAccessRequest(
|
|
284
|
+
db: Database,
|
|
285
|
+
input: ApproveSlackUserLinkAccessInput,
|
|
286
|
+
now = new Date(),
|
|
287
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
288
|
+
return await mutateRequest(
|
|
289
|
+
db,
|
|
290
|
+
input,
|
|
291
|
+
"approve",
|
|
292
|
+
{ role: input.role ?? "member", permissions: input.permissions },
|
|
293
|
+
now,
|
|
294
|
+
async (database, row) => {
|
|
295
|
+
if (row.status !== "pending") {
|
|
296
|
+
throw new SlackUserLinkAccessPersistenceError("state_conflict");
|
|
297
|
+
}
|
|
298
|
+
await assertSlackIdentityAvailable(database, row);
|
|
299
|
+
const role = input.role ?? "member";
|
|
300
|
+
await database
|
|
301
|
+
.insert(schema.workspaceMemberships)
|
|
302
|
+
.values({
|
|
303
|
+
accountId: row.accountId,
|
|
304
|
+
workspaceId: row.workspaceId,
|
|
305
|
+
subjectId: row.subjectId,
|
|
306
|
+
subjectLabel: row.subjectLabel,
|
|
307
|
+
role,
|
|
308
|
+
permissions: input.permissions,
|
|
309
|
+
})
|
|
310
|
+
.onConflictDoUpdate({
|
|
311
|
+
target: [schema.workspaceMemberships.subjectId, schema.workspaceMemberships.workspaceId],
|
|
312
|
+
set: {
|
|
313
|
+
subjectLabel: row.subjectLabel,
|
|
314
|
+
role,
|
|
315
|
+
permissions: input.permissions,
|
|
316
|
+
updatedAt: now,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
const membership = await membershipAllowsSlackLink(database, row);
|
|
320
|
+
if (!membership) throw new Error("Slack access approval did not create the required grant");
|
|
321
|
+
await upsertSlackIdentityLink(database, row, input.actorSubjectId, now);
|
|
322
|
+
const [updated] = await database
|
|
323
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
324
|
+
.set({
|
|
325
|
+
status: "completed",
|
|
326
|
+
version: row.version + 1,
|
|
327
|
+
decidedAt: now,
|
|
328
|
+
decisionBySubjectId: input.actorSubjectId,
|
|
329
|
+
approvedRole: role,
|
|
330
|
+
approvedPermissions: input.permissions,
|
|
331
|
+
completedAt: now,
|
|
332
|
+
updatedAt: now,
|
|
333
|
+
})
|
|
334
|
+
.where(
|
|
335
|
+
and(
|
|
336
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
337
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
.returning();
|
|
341
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
342
|
+
await insertAudit(
|
|
343
|
+
database,
|
|
344
|
+
updated,
|
|
345
|
+
input.actorSubjectId,
|
|
346
|
+
"workspace.access_request.approved",
|
|
347
|
+
{
|
|
348
|
+
membershipRole: role,
|
|
349
|
+
membershipPermissions: input.permissions,
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
await insertAudit(database, updated, input.actorSubjectId, "slack.user_link.completed");
|
|
353
|
+
return updated;
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Complete a prepared/pending link only when the subject's live membership
|
|
360
|
+
* already grants sessions:create (or workspace:admin). This is used both for
|
|
361
|
+
* ordinary already-authorized links and after a separate canonical member grant.
|
|
362
|
+
*/
|
|
363
|
+
export async function completeSlackUserLinkAccessIfGranted(
|
|
364
|
+
db: Database,
|
|
365
|
+
input: { workspaceId: string; requestId: string; subjectId: string },
|
|
366
|
+
now = new Date(),
|
|
367
|
+
): Promise<SlackUserLinkAccessRequest | null> {
|
|
368
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
369
|
+
return await scopedDb.transaction(async (tx) => {
|
|
370
|
+
const database = tx as unknown as Database;
|
|
371
|
+
const [found] = await tx
|
|
372
|
+
.select()
|
|
373
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
374
|
+
.where(
|
|
375
|
+
and(
|
|
376
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
|
|
377
|
+
eq(schema.slackUserLinkAccessRequests.id, input.requestId),
|
|
378
|
+
eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
.for("update")
|
|
382
|
+
.limit(1);
|
|
383
|
+
if (!found) return null;
|
|
384
|
+
const row = await expireLockedRequest(database, found, now);
|
|
385
|
+
if (row.status === "completed" || !isActiveStatus(row.status)) {
|
|
386
|
+
return mapRequest(row);
|
|
387
|
+
}
|
|
388
|
+
if (!(await membershipAllowsSlackLink(database, row))) {
|
|
389
|
+
return mapRequest(row);
|
|
390
|
+
}
|
|
391
|
+
await assertSlackIdentityAvailable(database, row);
|
|
392
|
+
await upsertSlackIdentityLink(database, row, input.subjectId, now);
|
|
393
|
+
const [updated] = await tx
|
|
394
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
395
|
+
.set({
|
|
396
|
+
status: "completed",
|
|
397
|
+
version: row.version + 1,
|
|
398
|
+
decidedAt: now,
|
|
399
|
+
decisionBySubjectId: input.subjectId,
|
|
400
|
+
completedAt: now,
|
|
401
|
+
updatedAt: now,
|
|
402
|
+
})
|
|
403
|
+
.where(
|
|
404
|
+
and(
|
|
405
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
406
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
.returning();
|
|
410
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
411
|
+
await insertAudit(database, updated, input.subjectId, "slack.user_link.completed");
|
|
412
|
+
return mapRequest(updated);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function listPendingSlackUserLinkAccessRequests(
|
|
418
|
+
db: Database,
|
|
419
|
+
workspaceId: string,
|
|
420
|
+
now = new Date(),
|
|
421
|
+
): Promise<SlackUserLinkAccessRequest[]> {
|
|
422
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
423
|
+
return await scopedDb.transaction(async (tx) => {
|
|
424
|
+
const database = tx as unknown as Database;
|
|
425
|
+
const expired = await tx
|
|
426
|
+
.select()
|
|
427
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
428
|
+
.where(
|
|
429
|
+
and(
|
|
430
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, workspaceId),
|
|
431
|
+
inArray(schema.slackUserLinkAccessRequests.status, activeStatuses),
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
.for("update");
|
|
435
|
+
for (const row of expired) {
|
|
436
|
+
if (row.expiresAt.getTime() <= now.getTime()) {
|
|
437
|
+
await expireLockedRequest(database, row, now);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const rows = await tx
|
|
441
|
+
.select()
|
|
442
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
443
|
+
.where(
|
|
444
|
+
and(
|
|
445
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, workspaceId),
|
|
446
|
+
eq(schema.slackUserLinkAccessRequests.status, "pending"),
|
|
447
|
+
gt(schema.slackUserLinkAccessRequests.expiresAt, now),
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
.orderBy(asc(schema.slackUserLinkAccessRequests.requestedAt));
|
|
451
|
+
return rows.map(mapRequest);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function mutateRequest(
|
|
457
|
+
db: Database,
|
|
458
|
+
input: SlackUserLinkAccessMutationInput,
|
|
459
|
+
operation: OperationKind,
|
|
460
|
+
protectedInput: Record<string, unknown>,
|
|
461
|
+
now: Date,
|
|
462
|
+
transition: (database: Database, row: RequestRow) => Promise<RequestRow>,
|
|
463
|
+
): Promise<SlackUserLinkAccessRequest> {
|
|
464
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
465
|
+
return await scopedDb.transaction(async (tx) => {
|
|
466
|
+
const database = tx as unknown as Database;
|
|
467
|
+
const requestDigest = digestOperation({
|
|
468
|
+
operation,
|
|
469
|
+
requestId: input.requestId,
|
|
470
|
+
actorSubjectId: input.actorSubjectId,
|
|
471
|
+
expectedVersion: input.expectedVersion,
|
|
472
|
+
...protectedInput,
|
|
473
|
+
});
|
|
474
|
+
const [found] = await tx
|
|
475
|
+
.select()
|
|
476
|
+
.from(schema.slackUserLinkAccessRequests)
|
|
477
|
+
.where(
|
|
478
|
+
and(
|
|
479
|
+
eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
|
|
480
|
+
eq(schema.slackUserLinkAccessRequests.id, input.requestId),
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
.for("update")
|
|
484
|
+
.limit(1);
|
|
485
|
+
if (!found) throw new SlackUserLinkAccessPersistenceError("not_found");
|
|
486
|
+
const [replay] = await tx
|
|
487
|
+
.select()
|
|
488
|
+
.from(schema.slackUserLinkAccessRequestOperations)
|
|
489
|
+
.where(
|
|
490
|
+
and(
|
|
491
|
+
eq(schema.slackUserLinkAccessRequestOperations.requestId, input.requestId),
|
|
492
|
+
eq(schema.slackUserLinkAccessRequestOperations.actorSubjectId, input.actorSubjectId),
|
|
493
|
+
eq(schema.slackUserLinkAccessRequestOperations.operation, operation),
|
|
494
|
+
eq(schema.slackUserLinkAccessRequestOperations.idempotencyKey, input.idempotencyKey),
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
.limit(1);
|
|
498
|
+
if (replay) {
|
|
499
|
+
if (
|
|
500
|
+
replay.requestDigest !== requestDigest ||
|
|
501
|
+
replay.expectedVersion !== input.expectedVersion
|
|
502
|
+
) {
|
|
503
|
+
throw new SlackUserLinkAccessPersistenceError("idempotency_conflict");
|
|
504
|
+
}
|
|
505
|
+
return SlackUserLinkAccessRequestSchema.parse(replay.result);
|
|
506
|
+
}
|
|
507
|
+
const row = await expireLockedRequest(database, found, now);
|
|
508
|
+
if (row.version !== input.expectedVersion) {
|
|
509
|
+
throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
510
|
+
}
|
|
511
|
+
const updated = await transition(database, row);
|
|
512
|
+
const result = mapRequest(updated);
|
|
513
|
+
await tx.insert(schema.slackUserLinkAccessRequestOperations).values({
|
|
514
|
+
accountId: updated.accountId,
|
|
515
|
+
workspaceId: updated.workspaceId,
|
|
516
|
+
requestId: updated.id,
|
|
517
|
+
actorSubjectId: input.actorSubjectId,
|
|
518
|
+
operation,
|
|
519
|
+
idempotencyKey: input.idempotencyKey,
|
|
520
|
+
requestDigest,
|
|
521
|
+
expectedVersion: input.expectedVersion,
|
|
522
|
+
resultVersion: updated.version,
|
|
523
|
+
resultStatus: updated.status,
|
|
524
|
+
result,
|
|
525
|
+
});
|
|
526
|
+
return result;
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function expireLockedRequest(database: Database, row: RequestRow, now: Date) {
|
|
532
|
+
if (!isActiveStatus(row.status) || row.expiresAt.getTime() > now.getTime()) {
|
|
533
|
+
return row;
|
|
534
|
+
}
|
|
535
|
+
const [updated] = await database
|
|
536
|
+
.update(schema.slackUserLinkAccessRequests)
|
|
537
|
+
.set({
|
|
538
|
+
status: "expired",
|
|
539
|
+
version: row.version + 1,
|
|
540
|
+
decidedAt: now,
|
|
541
|
+
updatedAt: now,
|
|
542
|
+
})
|
|
543
|
+
.where(
|
|
544
|
+
and(
|
|
545
|
+
eq(schema.slackUserLinkAccessRequests.id, row.id),
|
|
546
|
+
eq(schema.slackUserLinkAccessRequests.version, row.version),
|
|
547
|
+
),
|
|
548
|
+
)
|
|
549
|
+
.returning();
|
|
550
|
+
if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
|
|
551
|
+
await insertAudit(database, updated, null, "workspace.access_request.expired");
|
|
552
|
+
return updated;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function membershipAllowsSlackLink(database: Database, row: RequestRow) {
|
|
556
|
+
const [membership] = await database
|
|
557
|
+
.select({ permissions: schema.workspaceMemberships.permissions })
|
|
558
|
+
.from(schema.workspaceMemberships)
|
|
559
|
+
.where(
|
|
560
|
+
and(
|
|
561
|
+
eq(schema.workspaceMemberships.workspaceId, row.workspaceId),
|
|
562
|
+
eq(schema.workspaceMemberships.subjectId, row.subjectId),
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
.limit(1);
|
|
566
|
+
const permissions = membership?.permissions ?? [];
|
|
567
|
+
return permissions.includes("sessions:create") || permissions.includes("workspace:admin");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function assertSlackIdentityAvailable(database: Database, row: RequestRow) {
|
|
571
|
+
const [existing] = await database
|
|
572
|
+
.select({ subjectId: schema.slackBotUserLinks.subjectId })
|
|
573
|
+
.from(schema.slackBotUserLinks)
|
|
574
|
+
.where(
|
|
575
|
+
and(
|
|
576
|
+
eq(schema.slackBotUserLinks.connectionId, row.connectionId),
|
|
577
|
+
eq(schema.slackBotUserLinks.slackUserId, row.slackUserId),
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
.for("update")
|
|
581
|
+
.limit(1);
|
|
582
|
+
if (existing && existing.subjectId !== row.subjectId) {
|
|
583
|
+
throw new SlackUserLinkAccessPersistenceError("slack_identity_conflict");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function upsertSlackIdentityLink(
|
|
588
|
+
database: Database,
|
|
589
|
+
row: RequestRow,
|
|
590
|
+
actorSubjectId: string,
|
|
591
|
+
now: Date,
|
|
592
|
+
) {
|
|
593
|
+
const [created] = await database
|
|
594
|
+
.insert(schema.slackBotUserLinks)
|
|
595
|
+
.values({
|
|
596
|
+
accountId: row.accountId,
|
|
597
|
+
workspaceId: row.workspaceId,
|
|
598
|
+
connectionId: row.connectionId,
|
|
599
|
+
slackTeamId: row.slackTeamId,
|
|
600
|
+
slackUserId: row.slackUserId,
|
|
601
|
+
subjectId: row.subjectId,
|
|
602
|
+
linkedBySubjectId: actorSubjectId,
|
|
603
|
+
})
|
|
604
|
+
.onConflictDoNothing({
|
|
605
|
+
target: [schema.slackBotUserLinks.connectionId, schema.slackBotUserLinks.slackUserId],
|
|
606
|
+
})
|
|
607
|
+
.returning({ id: schema.slackBotUserLinks.id });
|
|
608
|
+
if (created) return;
|
|
609
|
+
|
|
610
|
+
const [existing] = await database
|
|
611
|
+
.select({ id: schema.slackBotUserLinks.id, subjectId: schema.slackBotUserLinks.subjectId })
|
|
612
|
+
.from(schema.slackBotUserLinks)
|
|
613
|
+
.where(
|
|
614
|
+
and(
|
|
615
|
+
eq(schema.slackBotUserLinks.connectionId, row.connectionId),
|
|
616
|
+
eq(schema.slackBotUserLinks.slackUserId, row.slackUserId),
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
.for("update")
|
|
620
|
+
.limit(1);
|
|
621
|
+
if (!existing || existing.subjectId !== row.subjectId) {
|
|
622
|
+
throw new SlackUserLinkAccessPersistenceError("slack_identity_conflict");
|
|
623
|
+
}
|
|
624
|
+
await database
|
|
625
|
+
.update(schema.slackBotUserLinks)
|
|
626
|
+
.set({
|
|
627
|
+
accountId: row.accountId,
|
|
628
|
+
workspaceId: row.workspaceId,
|
|
629
|
+
slackTeamId: row.slackTeamId,
|
|
630
|
+
linkedBySubjectId: actorSubjectId,
|
|
631
|
+
updatedAt: now,
|
|
632
|
+
})
|
|
633
|
+
.where(
|
|
634
|
+
and(
|
|
635
|
+
eq(schema.slackBotUserLinks.id, existing.id),
|
|
636
|
+
eq(schema.slackBotUserLinks.subjectId, row.subjectId),
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function insertAudit(
|
|
642
|
+
database: Database,
|
|
643
|
+
row: RequestRow,
|
|
644
|
+
actorSubjectId: string | null,
|
|
645
|
+
action: string,
|
|
646
|
+
metadata: Record<string, unknown> = {},
|
|
647
|
+
) {
|
|
648
|
+
await database.insert(schema.auditEvents).values(
|
|
649
|
+
withLosslessContentWriteVersion(
|
|
650
|
+
{
|
|
651
|
+
accountId: row.accountId,
|
|
652
|
+
workspaceId: row.workspaceId,
|
|
653
|
+
subjectId: actorSubjectId,
|
|
654
|
+
action,
|
|
655
|
+
targetType: "slack_user_link_access_request",
|
|
656
|
+
targetId: row.id,
|
|
657
|
+
metadata: {
|
|
658
|
+
status: row.status,
|
|
659
|
+
version: row.version,
|
|
660
|
+
connectionId: row.connectionId,
|
|
661
|
+
...metadata,
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
"metadata",
|
|
665
|
+
"metadataCodecVersion",
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function assertExactClaims(row: RequestRow, input: PrepareSlackUserLinkAccessInput) {
|
|
671
|
+
if (
|
|
672
|
+
row.accountId !== input.accountId ||
|
|
673
|
+
row.workspaceId !== input.workspaceId ||
|
|
674
|
+
row.connectionId !== input.connectionId ||
|
|
675
|
+
row.slackTeamId !== input.slackTeamId ||
|
|
676
|
+
row.slackUserId !== input.slackUserId ||
|
|
677
|
+
row.expiresAt.getTime() !== input.expiresAt.getTime()
|
|
678
|
+
) {
|
|
679
|
+
throw new SlackUserLinkAccessPersistenceError("claim_mismatch");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function digestOperation(value: Record<string, unknown>) {
|
|
684
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function mapRequest(row: RequestRow): SlackUserLinkAccessRequest {
|
|
688
|
+
return {
|
|
689
|
+
id: row.id,
|
|
690
|
+
workspaceId: row.workspaceId,
|
|
691
|
+
workspaceDisplayName: null,
|
|
692
|
+
subjectLabel: row.subjectLabel,
|
|
693
|
+
status: row.status as SlackUserLinkAccessRequestStatus,
|
|
694
|
+
version: row.version,
|
|
695
|
+
expiresAt: row.expiresAt.toISOString(),
|
|
696
|
+
requestedAt: row.requestedAt?.toISOString() ?? null,
|
|
697
|
+
decidedAt: row.decidedAt?.toISOString() ?? null,
|
|
698
|
+
completedAt: row.completedAt?.toISOString() ?? null,
|
|
699
|
+
createdAt: row.createdAt.toISOString(),
|
|
700
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
701
|
+
};
|
|
702
|
+
}
|