@agent-native/core 0.201.1 → 0.202.0-nightly-20261006033736

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/automation/index.d.ts +1 -1
  2. package/dist/client/analytics/index.d.ts +1 -1
  3. package/dist/client/analytics/index.js +1 -1
  4. package/dist/client/analytics.d.ts +23 -3
  5. package/dist/client/analytics.js +189 -46
  6. package/dist/client/client-status-requests.d.ts +4 -1
  7. package/dist/client/client-status-requests.js +21 -5
  8. package/dist/client/embed-auth.d.ts +6 -0
  9. package/dist/client/embed-auth.js +13 -0
  10. package/dist/client/session-replay.js +48 -22
  11. package/dist/client/sharing/index.d.ts +2 -0
  12. package/dist/client/sharing/index.js +2 -0
  13. package/dist/client/sharing/useAccessRequestReview.d.ts +80 -0
  14. package/dist/client/sharing/useAccessRequestReview.js +100 -0
  15. package/dist/client/sharing/useResourceAccessGate.d.ts +57 -0
  16. package/dist/client/sharing/useResourceAccessGate.js +76 -0
  17. package/dist/client/use-agent-engine-configured.d.ts +2 -0
  18. package/dist/client/use-agent-engine-configured.js +4 -1
  19. package/dist/email-catalog/registry.d.ts +1 -1
  20. package/dist/email-catalog/registry.js +1 -1
  21. package/dist/email-catalog/system-emails.d.ts +2 -2
  22. package/dist/email-catalog/system-emails.js +47 -2
  23. package/dist/email-catalog/templates.d.ts +5 -1
  24. package/dist/email-catalog/templates.js +11 -1
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/framework-tools.js +6 -0
  27. package/dist/identity/rekey.js +9 -0
  28. package/dist/jobs/run-history.d.ts +27 -8
  29. package/dist/jobs/run-history.js +17 -10
  30. package/dist/jobs/scheduler.js +39 -10
  31. package/dist/mcp/build-server.d.ts +1 -0
  32. package/dist/mcp/build-server.js +30 -7
  33. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  34. package/dist/notifications/routes.d.ts +2 -2
  35. package/dist/observability/routes.d.ts +7 -7
  36. package/dist/org/index.d.ts +1 -1
  37. package/dist/progress/routes.d.ts +1 -1
  38. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  39. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  40. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  41. package/dist/review/suggestions/actions.js +14 -35
  42. package/dist/review/suggestions/store.d.ts +0 -1
  43. package/dist/review/suggestions/store.js +0 -7
  44. package/dist/secrets/routes.d.ts +11 -11
  45. package/dist/server/action-discovery.js +24 -0
  46. package/dist/server/agent-access.js +7 -3
  47. package/dist/server/agent-chat/mcp-options.d.ts +1 -0
  48. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  49. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  50. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  51. package/dist/server/attribution.d.ts +33 -1
  52. package/dist/server/attribution.js +93 -21
  53. package/dist/server/auth.js +35 -11
  54. package/dist/server/email-templates.d.ts +34 -0
  55. package/dist/server/email-templates.js +40 -0
  56. package/dist/server/magic-link-attribution.js +2 -1
  57. package/dist/server/onboarding-html.js +35 -67
  58. package/dist/server/release-schema.js +4 -0
  59. package/dist/server/short-lived-token.d.ts +7 -0
  60. package/dist/server/short-lived-token.js +59 -0
  61. package/dist/shared/analytics-anonymous-id.d.ts +1 -0
  62. package/dist/shared/analytics-anonymous-id.js +1 -1
  63. package/dist/shared/attribution-source.d.ts +30 -0
  64. package/dist/shared/attribution-source.js +65 -0
  65. package/dist/sharing/access-request-store.d.ts +96 -0
  66. package/dist/sharing/access-request-store.js +213 -0
  67. package/dist/sharing/access-requests.d.ts +113 -0
  68. package/dist/sharing/access-requests.js +490 -0
  69. package/dist/sharing/access.d.ts +36 -2
  70. package/dist/sharing/access.js +74 -12
  71. package/dist/sharing/actions/approve-resource-access-request.d.ts +10 -0
  72. package/dist/sharing/actions/approve-resource-access-request.js +15 -0
  73. package/dist/sharing/actions/decline-resource-access-request.d.ts +7 -0
  74. package/dist/sharing/actions/decline-resource-access-request.js +14 -0
  75. package/dist/sharing/actions/get-resource-access-request.d.ts +4 -0
  76. package/dist/sharing/actions/get-resource-access-request.js +13 -0
  77. package/dist/sharing/actions/get-resource-access-status.d.ts +5 -0
  78. package/dist/sharing/actions/get-resource-access-status.js +19 -0
  79. package/dist/sharing/actions/list-resource-access-requests.d.ts +5 -0
  80. package/dist/sharing/actions/list-resource-access-requests.js +16 -0
  81. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  82. package/dist/sharing/actions/request-resource-access.d.ts +6 -0
  83. package/dist/sharing/actions/request-resource-access.js +17 -0
  84. package/dist/sharing/actions/share-resource.d.ts +1 -1
  85. package/dist/sharing/actions/share-resource.js +18 -144
  86. package/dist/sharing/grant.d.ts +49 -0
  87. package/dist/sharing/grant.js +195 -0
  88. package/dist/sharing/index.d.ts +2 -1
  89. package/dist/sharing/index.js +2 -1
  90. package/dist/sharing/registry.d.ts +36 -0
  91. package/dist/templates/chat/server/plugins/auth.ts +2 -2
  92. package/dist/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  93. package/dist/templates/default/server/plugins/auth.ts +2 -2
  94. package/dist/vite/action-types-plugin.js +24 -0
  95. package/package.json +11 -3
  96. package/src/templates/chat/server/plugins/auth.ts +2 -2
  97. package/src/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  98. package/src/templates/default/server/plugins/auth.ts +2 -2
@@ -0,0 +1,10 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ requestId: string;
3
+ generation: number;
4
+ role?: "admin" | "commenter" | "editor" | "viewer";
5
+ }, {
6
+ state: "approved";
7
+ role: import("../schema.js").ShareRole;
8
+ email: import("../access-requests.js").AccessGrantedEmail;
9
+ }>;
10
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { approveAccessRequest } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "Allow a pending access request, giving the requester a role on the resource under the same sharing rules as Share. A stronger role they already hold is kept. `email` says whether the requester was emailed: sent, skipped, or failed.",
6
+ schema: z.object({
7
+ requestId: z.string().min(1),
8
+ generation: z.number().int().positive(),
9
+ role: z.enum(["viewer", "commenter", "editor", "admin"]).default("viewer"),
10
+ }),
11
+ agentTool: false,
12
+ mcpTool: false,
13
+ toolCallable: false,
14
+ run: (args) => approveAccessRequest(args),
15
+ });
@@ -0,0 +1,7 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ requestId: string;
3
+ generation: number;
4
+ }, {
5
+ state: "declined";
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { declineAccessRequest } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "Decline a pending access request. The requester isn't told, and can ask again after a cooldown.",
6
+ schema: z.object({
7
+ requestId: z.string().min(1),
8
+ generation: z.number().int().positive(),
9
+ }),
10
+ agentTool: false,
11
+ mcpTool: false,
12
+ toolCallable: false,
13
+ run: (args) => declineAccessRequest(args),
14
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ requestId: string;
3
+ }, import("../access-requests.js").AccessRequestReview>;
4
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getAccessRequestReview } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "Read one access request for review: who asked, for which resource, and whether it is still pending. Only someone who manages access to the resource can read it.",
6
+ schema: z.object({ requestId: z.string().min(1) }),
7
+ http: { method: "GET" },
8
+ readOnly: true,
9
+ agentTool: false,
10
+ mcpTool: false,
11
+ toolCallable: false,
12
+ run: ({ requestId }) => getAccessRequestReview(requestId),
13
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ resourceType: string;
3
+ resourceId: string;
4
+ }, import("../access-requests.js").ResourceLinkStatus>;
5
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { resolveLinkStatus } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "Say whether the current viewer can open a shareable resource, and if not, whether the link points at something that exists and whether they can ask for access. Returns only the state, the viewer's own role, and their own request; never the title, owner, visibility, or workspace.",
6
+ schema: z.object({
7
+ resourceType: z.string().min(1),
8
+ resourceId: z.string().min(1),
9
+ }),
10
+ http: { method: "GET" },
11
+ readOnly: true,
12
+ // A signed-out visitor holding a link gets `signed-out` and nothing else.
13
+ requiresAuth: false,
14
+ // Only the app's own access screens ask; agents read resources directly.
15
+ agentTool: false,
16
+ mcpTool: false,
17
+ toolCallable: false,
18
+ run: async ({ resourceType, resourceId }) => resolveLinkStatus(resourceType, resourceId),
19
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ resourceType: string;
3
+ resourceId: string;
4
+ }, import("../access-requests.js").ResourceAccessRequestList>;
5
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { listResourceAccessRequests } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "List the newest pending access requests for a shareable resource, up to 50; `hasMore` says older ones exist. Only someone who manages access to it can list them.",
6
+ schema: z.object({
7
+ resourceType: z.string().min(1),
8
+ resourceId: z.string().min(1),
9
+ }),
10
+ http: { method: "GET" },
11
+ readOnly: true,
12
+ agentTool: false,
13
+ mcpTool: false,
14
+ toolCallable: false,
15
+ run: ({ resourceType, resourceId }) => listResourceAccessRequests(resourceType, resourceId),
16
+ });
@@ -2,6 +2,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
2
2
  resourceType: string;
3
3
  resourceId: string;
4
4
  }, {
5
+ role?: undefined;
5
6
  ownerEmail: any;
6
7
  visibility: any;
7
8
  shares: any[];
@@ -11,7 +12,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
11
12
  supportsGroupShares?: boolean;
12
13
  };
13
14
  orgId?: undefined;
14
- role?: undefined;
15
15
  agentReadable?: undefined;
16
16
  } | {
17
17
  ownerEmail: string;
@@ -0,0 +1,6 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ resourceType: string;
3
+ resourceId: string;
4
+ note?: string;
5
+ }, import("../access-requests.js").RequestResourceAccessResult>;
6
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { ACCESS_REQUEST_NOTE_MAX_LENGTH, requestResourceAccess, } from "../access-requests.js";
4
+ export default defineAction({
5
+ description: "Ask the owner and admins of a shareable resource the signed-in viewer can't open to give them access. Asking again while a request is open sends nothing.",
6
+ schema: z.object({
7
+ resourceType: z.string().min(1),
8
+ resourceId: z.string().min(1),
9
+ note: z.string().max(ACCESS_REQUEST_NOTE_MAX_LENGTH).optional(),
10
+ }),
11
+ // It emails other people, so only the person holding the link asks, from
12
+ // the access screen.
13
+ agentTool: false,
14
+ mcpTool: false,
15
+ toolCallable: false,
16
+ run: (args) => requestResourceAccess(args),
17
+ });
@@ -9,7 +9,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
9
9
  resourceUrl?: string;
10
10
  message?: string;
11
11
  }, {
12
- id: any;
12
+ id: string;
13
13
  updated: boolean;
14
14
  change?: import("../../action-ui.js").ActionChange;
15
15
  }>;
@@ -1,21 +1,17 @@
1
- import { and, eq, ne, sql } from "drizzle-orm";
1
+ import { and, eq } from "drizzle-orm";
2
2
  import { z } from "zod";
3
- import { defineAction, fail } from "../../action.js";
3
+ import { defineAction } from "../../action.js";
4
4
  import { getAppConfig } from "../../app-config/index.js";
5
- import { getDbExec } from "../../db/client.js";
6
5
  import { CORE_RESOURCE_SHARED_EMAIL_ID, renderTransactionalEmail, } from "../../email-catalog/templates.js";
7
- import { isOrgMember } from "../../org/membership.js";
8
6
  import { getAppProductionUrl } from "../../server/app-url.js";
9
7
  import { sendEmail, isEmailConfigured } from "../../server/email.js";
10
- import { invalidateCollabAccessCache } from "../../server/poll.js";
11
8
  import { getRequestUserEmail } from "../../server/request-context.js";
12
9
  import { track } from "../../tracking/registry.js";
13
10
  import { getUserProfile } from "../../user-profile/store.js";
14
- import { assertWorkspaceUserGroupIds } from "../../workspace-connections/groups.js";
15
- import { assertAccess, ForbiddenError } from "../access.js";
11
+ import { assertAccess } from "../access.js";
12
+ import { announceResourceAccessChange, grantResourceAccess, isEmailPrincipalId, isOrgMemberOrInvited, normalizePrincipalId, principalIdMatches, } from "../grant.js";
16
13
  import { requireShareableResource } from "../registry.js";
17
14
  import { resourceSharingChange } from "./change-result.js";
18
- import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
19
15
  function appPath(path) {
20
16
  if (!path.startsWith("/"))
21
17
  return path;
@@ -65,40 +61,6 @@ export function resolveShareNotificationUrl(explicitUrl, fallbackPath, appUrl =
65
61
  }
66
62
  return appUrl;
67
63
  }
68
- function nanoid(size = 12) {
69
- const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
70
- let id = "";
71
- const bytes = crypto.getRandomValues(new Uint8Array(size));
72
- for (const byte of bytes)
73
- id += chars[byte % chars.length];
74
- return id;
75
- }
76
- function normalizePrincipalId(principalType, principalId) {
77
- return principalType === "user"
78
- ? principalId.trim().toLowerCase()
79
- : principalId;
80
- }
81
- function isEmailPrincipalId(value) {
82
- return /^[^\s@]+@[^\s@]+$/.test(value.trim());
83
- }
84
- function principalIdMatches(sharesTable, principalType, principalId) {
85
- return principalType === "user"
86
- ? sql `lower(${sharesTable.principalId}) = ${principalId}`
87
- : eq(sharesTable.principalId, principalId);
88
- }
89
- async function isOrgMemberOrInvited(orgId, email) {
90
- const lower = email.trim().toLowerCase();
91
- if (!lower || !orgId)
92
- return false;
93
- const client = getDbExec();
94
- if (await isOrgMember(orgId, lower))
95
- return true;
96
- const invited = await client.execute({
97
- sql: `SELECT 1 FROM org_invitations WHERE org_id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,
98
- args: [orgId, lower],
99
- });
100
- return invited.rows.length > 0;
101
- }
102
64
  async function needsExternalShareApproval(args) {
103
65
  if (args.principalType === "group")
104
66
  return false;
@@ -163,116 +125,28 @@ export default defineAction({
163
125
  needsApproval: needsExternalShareApproval,
164
126
  run: async (args) => {
165
127
  const reg = requireShareableResource(args.resourceType);
166
- const access = await assertAccess(args.resourceType, args.resourceId, "admin");
167
- const actor = getRequestUserEmail();
168
- if (!actor)
169
- throw new ForbiddenError("Not signed in");
170
- const principalId = normalizePrincipalId(args.principalType, args.principalId);
171
- if (args.principalType === "group" && reg.supportsGroupShares !== true) {
172
- throw new ForbiddenError(`${reg.displayName} does not support organization groups yet.`);
173
- }
174
- if (args.principalType === "user" && !isEmailPrincipalId(principalId)) {
175
- fail("User shares must use an email address, not an internal user id.", {
176
- errorCode: "invalid_user_share_principal",
177
- });
178
- }
179
- if (args.principalType === "group") {
180
- const resourceOrgId = access.resource?.orgId;
181
- if (!resourceOrgId) {
182
- throw new ForbiddenError(`${reg.displayName} can only be shared with a group from within an organization.`);
183
- }
184
- try {
185
- await assertWorkspaceUserGroupIds([principalId], resourceOrgId);
186
- }
187
- catch {
188
- throw new ForbiddenError(`${reg.displayName} can only be shared with a group from its own organization.`);
189
- }
190
- }
191
- const beforeExtensionTargets = await getExtensionShareChangeTargets(args.resourceType, args.resourceId);
192
- if (reg.requireOrgMemberForUserShares) {
193
- const resourceOrgId = access.resource?.orgId;
194
- if (!resourceOrgId) {
195
- throw new ForbiddenError(`${reg.displayName} can only be shared from within an organization. Create or join an organization first.`);
196
- }
197
- if (args.principalType === "user") {
198
- const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);
199
- if (!ok) {
200
- throw new ForbiddenError(`${principalId} is not in your organization. Invite them to the organization first, then share.`);
201
- }
202
- }
203
- else if (args.principalType === "org") {
204
- if (principalId !== resourceOrgId) {
205
- throw new ForbiddenError(`${reg.displayName} can only be shared with its own organization, not a different one.`);
206
- }
207
- }
208
- else if (args.principalType === "group") {
209
- // Group ownership was validated above against the resource org.
210
- }
211
- }
212
- const db = reg.getDb();
213
- const [existing] = await db
214
- .select()
215
- .from(reg.sharesTable)
216
- .where(and(eq(reg.sharesTable.resourceId, args.resourceId), eq(reg.sharesTable.principalType, args.principalType), principalIdMatches(reg.sharesTable, args.principalType, principalId)));
217
- if (existing) {
218
- const [updated] = await db
219
- .update(reg.sharesTable)
220
- .set({ role: args.role })
221
- .where(and(eq(reg.sharesTable.id, existing.id), ne(reg.sharesTable.role, args.role)))
222
- .returning({ id: reg.sharesTable.id });
223
- invalidateCollabAccessCache(args.resourceType, args.resourceId);
224
- await notifyExtensionShareChanged(args.resourceType, args.resourceId, beforeExtensionTargets);
225
- return {
226
- id: existing.id,
227
- updated: Boolean(updated),
228
- ...(updated
229
- ? {
230
- change: resourceSharingChange(reg, access.resource, "updated", `${args.principalType}:${principalId} · ${args.role}`).change,
231
- }
232
- : {}),
233
- };
234
- }
235
- const id = nanoid();
236
- const [inserted] = await db
237
- .insert(reg.sharesTable)
238
- .values({
239
- id,
128
+ const grant = await grantResourceAccess({
129
+ resourceType: args.resourceType,
240
130
  resourceId: args.resourceId,
241
131
  principalType: args.principalType,
242
- principalId,
132
+ principalId: args.principalId,
243
133
  role: args.role,
244
- createdBy: actor,
245
- createdAt: new Date().toISOString(),
246
- })
247
- .onConflictDoNothing()
248
- .returning({ id: reg.sharesTable.id });
249
- if (!inserted) {
250
- const [existingAfterConflict] = await db
251
- .select()
252
- .from(reg.sharesTable)
253
- .where(and(eq(reg.sharesTable.resourceId, args.resourceId), eq(reg.sharesTable.principalType, args.principalType), principalIdMatches(reg.sharesTable, args.principalType, principalId)));
254
- if (!existingAfterConflict) {
255
- throw new Error("Share conflict could not be resolved.");
256
- }
257
- const [updated] = await db
258
- .update(reg.sharesTable)
259
- .set({ role: args.role })
260
- .where(and(eq(reg.sharesTable.id, existingAfterConflict.id), ne(reg.sharesTable.role, args.role)))
261
- .returning({ id: reg.sharesTable.id });
262
- invalidateCollabAccessCache(args.resourceType, args.resourceId);
263
- await notifyExtensionShareChanged(args.resourceType, args.resourceId, beforeExtensionTargets);
134
+ });
135
+ const actor = getRequestUserEmail();
136
+ const { id, principalId, resource: accessResource } = grant;
137
+ await announceResourceAccessChange(args.resourceType, args.resourceId, grant.extensionTargetsBefore);
138
+ if (!grant.created) {
264
139
  return {
265
- id: existingAfterConflict.id,
266
- updated: Boolean(updated),
267
- ...(updated
140
+ id,
141
+ updated: grant.updated,
142
+ ...(grant.updated
268
143
  ? {
269
- change: resourceSharingChange(reg, access.resource, "updated", `${args.principalType}:${principalId} · ${args.role}`).change,
144
+ change: resourceSharingChange(reg, accessResource, "updated", `${args.principalType}:${principalId} · ${args.role}`).change,
270
145
  }
271
146
  : {}),
272
147
  };
273
148
  }
274
- invalidateCollabAccessCache(args.resourceType, args.resourceId);
275
- await notifyExtensionShareChanged(args.resourceType, args.resourceId, beforeExtensionTargets);
149
+ const db = reg.getDb();
276
150
  const shouldNotify = args.notify !== false &&
277
151
  args.principalType === "user" &&
278
152
  (await isEmailConfigured());
@@ -408,7 +282,7 @@ export default defineAction({
408
282
  return {
409
283
  id,
410
284
  updated: false,
411
- change: resourceSharingChange(reg, access.resource, "created", `${args.principalType}:${principalId} · ${args.role}`).change,
285
+ change: resourceSharingChange(reg, accessResource, "created", `${args.principalType}:${principalId} · ${args.role}`).change,
412
286
  };
413
287
  },
414
288
  });
@@ -0,0 +1,49 @@
1
+ import { type SQL } from "drizzle-orm";
2
+ import type { ExtensionChangeTarget } from "../extensions/change-marker.js";
3
+ import { type PrincipalType, type ShareRole } from "./schema.js";
4
+ export declare function normalizePrincipalId(principalType: PrincipalType, principalId: string): string;
5
+ export declare function isEmailPrincipalId(value: string): boolean;
6
+ export declare function principalIdMatches(sharesTable: any, principalType: PrincipalType, principalId: string): SQL;
7
+ export declare function isOrgMemberOrInvited(orgId: string, email: string): Promise<boolean>;
8
+ export interface GrantResourceAccessInput {
9
+ resourceType: string;
10
+ resourceId: string;
11
+ principalType: PrincipalType;
12
+ principalId: string;
13
+ role: ShareRole;
14
+ /**
15
+ * Keep a stronger role the principal already holds instead of replacing it.
16
+ * Share replaces roles in both directions; approving a request only raises.
17
+ */
18
+ keepStrongerRole?: boolean;
19
+ }
20
+ export interface GrantResourceAccessResult {
21
+ /** The principal's share row. */
22
+ id: string;
23
+ /** A new share row was inserted. */
24
+ created: boolean;
25
+ /** An existing share row's role changed. */
26
+ updated: boolean;
27
+ /** The role the share row holds now. */
28
+ role: ShareRole;
29
+ principalId: string;
30
+ /** The resource as the actor's admin access check loaded it. */
31
+ resource: any;
32
+ /** Pass to {@link announceResourceAccessChange} once the grant commits. */
33
+ extensionTargetsBefore: ExtensionChangeTarget[];
34
+ }
35
+ /**
36
+ * Gives a principal a role on a resource, as the signed-in actor. The actor
37
+ * needs admin on the resource, and every sharing rule applies: group support,
38
+ * organization-only sharing, and the principal's form. Share and approving an
39
+ * access request both grant through here, so neither can skip a rule.
40
+ *
41
+ * It only writes the share row. Call {@link announceResourceAccessChange}
42
+ * afterwards, outside any transaction, so open sessions see the change.
43
+ */
44
+ export declare function grantResourceAccess(input: GrantResourceAccessInput): Promise<GrantResourceAccessResult>;
45
+ /**
46
+ * Tells open sessions that a resource's sharing changed, after the grant that
47
+ * changed it has committed.
48
+ */
49
+ export declare function announceResourceAccessChange(resourceType: string, resourceId: string, extensionTargetsBefore: ExtensionChangeTarget[]): Promise<void>;
@@ -0,0 +1,195 @@
1
+ import { and, eq, inArray, ne, sql } from "drizzle-orm";
2
+ import { drizzle as drizzleProxy } from "drizzle-orm/pg-proxy";
3
+ import { fail } from "../action.js";
4
+ import { getDbExec, getScopedDbExec } from "../db/client.js";
5
+ import { isOrgMember } from "../org/membership.js";
6
+ import { invalidateCollabAccessCache } from "../server/poll.js";
7
+ import { getRequestUserEmail } from "../server/request-context.js";
8
+ import { assertWorkspaceUserGroupIds } from "../workspace-connections/groups.js";
9
+ import { assertAccess, ForbiddenError } from "./access.js";
10
+ import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./actions/extension-change.js";
11
+ import { requireShareableResource, } from "./registry.js";
12
+ import { ROLE_RANK } from "./schema.js";
13
+ export function normalizePrincipalId(principalType, principalId) {
14
+ return principalType === "user"
15
+ ? principalId.trim().toLowerCase()
16
+ : principalId;
17
+ }
18
+ export function isEmailPrincipalId(value) {
19
+ return /^[^\s@]+@[^\s@]+$/.test(value.trim());
20
+ }
21
+ export function principalIdMatches(sharesTable, principalType, principalId) {
22
+ return principalType === "user"
23
+ ? sql `lower(${sharesTable.principalId}) = ${principalId}`
24
+ : eq(sharesTable.principalId, principalId);
25
+ }
26
+ export async function isOrgMemberOrInvited(orgId, email) {
27
+ const lower = email.trim().toLowerCase();
28
+ if (!lower || !orgId)
29
+ return false;
30
+ const client = getDbExec();
31
+ if (await isOrgMember(orgId, lower))
32
+ return true;
33
+ const invited = await client.execute({
34
+ sql: `SELECT 1 FROM org_invitations WHERE org_id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,
35
+ args: [orgId, lower],
36
+ });
37
+ return invited.rows.length > 0;
38
+ }
39
+ function nanoid(size = 12) {
40
+ const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
41
+ let id = "";
42
+ const bytes = crypto.getRandomValues(new Uint8Array(size));
43
+ for (const byte of bytes)
44
+ id += chars[byte % chars.length];
45
+ return id;
46
+ }
47
+ // Inside a transaction (see `withDbExec`), share rows are written through the
48
+ // transaction's handle so they commit or roll back with everything else.
49
+ function sharesDb(reg) {
50
+ const transaction = getScopedDbExec();
51
+ if (!transaction)
52
+ return reg.getDb();
53
+ return drizzleProxy(async (query, params) => {
54
+ const result = await transaction.execute({ sql: query, args: params });
55
+ return { rows: result.rows.map((row) => Object.values(row)) };
56
+ });
57
+ }
58
+ const SHARE_ROLES = ["viewer", "commenter", "editor", "admin"];
59
+ function weakerRoles(role) {
60
+ return SHARE_ROLES.filter((other) => ROLE_RANK[other] < ROLE_RANK[role]);
61
+ }
62
+ /**
63
+ * Gives a principal a role on a resource, as the signed-in actor. The actor
64
+ * needs admin on the resource, and every sharing rule applies: group support,
65
+ * organization-only sharing, and the principal's form. Share and approving an
66
+ * access request both grant through here, so neither can skip a rule.
67
+ *
68
+ * It only writes the share row. Call {@link announceResourceAccessChange}
69
+ * afterwards, outside any transaction, so open sessions see the change.
70
+ */
71
+ export async function grantResourceAccess(input) {
72
+ const reg = requireShareableResource(input.resourceType);
73
+ const access = await assertAccess(input.resourceType, input.resourceId, "admin");
74
+ const actor = getRequestUserEmail();
75
+ if (!actor)
76
+ throw new ForbiddenError("Not signed in");
77
+ const principalId = normalizePrincipalId(input.principalType, input.principalId);
78
+ if (input.principalType === "group" && reg.supportsGroupShares !== true) {
79
+ throw new ForbiddenError(`${reg.displayName} does not support organization groups yet.`);
80
+ }
81
+ if (input.principalType === "user" && !isEmailPrincipalId(principalId)) {
82
+ fail("User shares must use an email address, not an internal user id.", {
83
+ errorCode: "invalid_user_share_principal",
84
+ });
85
+ }
86
+ if (input.principalType === "group") {
87
+ const resourceOrgId = access.resource?.orgId;
88
+ if (!resourceOrgId) {
89
+ throw new ForbiddenError(`${reg.displayName} can only be shared with a group from within an organization.`);
90
+ }
91
+ try {
92
+ await assertWorkspaceUserGroupIds([principalId], resourceOrgId);
93
+ }
94
+ catch {
95
+ throw new ForbiddenError(`${reg.displayName} can only be shared with a group from its own organization.`);
96
+ }
97
+ }
98
+ const extensionTargetsBefore = await getExtensionShareChangeTargets(input.resourceType, input.resourceId);
99
+ if (reg.requireOrgMemberForUserShares) {
100
+ const resourceOrgId = access.resource?.orgId;
101
+ if (!resourceOrgId) {
102
+ throw new ForbiddenError(`${reg.displayName} can only be shared from within an organization. Create or join an organization first.`);
103
+ }
104
+ if (input.principalType === "user") {
105
+ const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);
106
+ if (!ok) {
107
+ throw new ForbiddenError(`${principalId} is not in your organization. Invite them to the organization first, then share.`);
108
+ }
109
+ }
110
+ else if (input.principalType === "org") {
111
+ if (principalId !== resourceOrgId) {
112
+ throw new ForbiddenError(`${reg.displayName} can only be shared with its own organization, not a different one.`);
113
+ }
114
+ }
115
+ }
116
+ const db = sharesDb(reg);
117
+ const findExisting = async () => {
118
+ const [row] = await db
119
+ .select({ id: reg.sharesTable.id, role: reg.sharesTable.role })
120
+ .from(reg.sharesTable)
121
+ .where(and(eq(reg.sharesTable.resourceId, input.resourceId), eq(reg.sharesTable.principalType, input.principalType), principalIdMatches(reg.sharesTable, input.principalType, principalId)));
122
+ return row;
123
+ };
124
+ const setRole = async (existing) => {
125
+ const keep = input.keepStrongerRole === true &&
126
+ ROLE_RANK[existing.role] >= ROLE_RANK[input.role];
127
+ if (keep) {
128
+ return { id: existing.id, updated: false, role: existing.role };
129
+ }
130
+ // The role can change between the read above and this write, so keeping a
131
+ // stronger role is checked by the write itself, not only by the read.
132
+ const replaceable = input.keepStrongerRole
133
+ ? inArray(reg.sharesTable.role, weakerRoles(input.role))
134
+ : ne(reg.sharesTable.role, input.role);
135
+ const [updated] = await db
136
+ .update(reg.sharesTable)
137
+ .set({ role: input.role })
138
+ .where(and(eq(reg.sharesTable.id, existing.id), replaceable))
139
+ .returning({ id: reg.sharesTable.id });
140
+ if (updated || !input.keepStrongerRole) {
141
+ return { id: existing.id, updated: Boolean(updated), role: input.role };
142
+ }
143
+ const current = await findExisting();
144
+ if (!current) {
145
+ fail("Access changed while it was being granted. Try again.", {
146
+ errorCode: "share_conflict",
147
+ statusCode: 409,
148
+ });
149
+ }
150
+ return { id: current.id, updated: false, role: current.role };
151
+ };
152
+ const result = {
153
+ principalId,
154
+ resource: access.resource,
155
+ extensionTargetsBefore,
156
+ };
157
+ const existing = await findExisting();
158
+ if (existing) {
159
+ return { ...result, ...(await setRole(existing)), created: false };
160
+ }
161
+ const id = nanoid();
162
+ const [inserted] = await db
163
+ .insert(reg.sharesTable)
164
+ .values({
165
+ id,
166
+ resourceId: input.resourceId,
167
+ principalType: input.principalType,
168
+ principalId,
169
+ role: input.role,
170
+ createdBy: actor,
171
+ createdAt: new Date().toISOString(),
172
+ })
173
+ .onConflictDoNothing()
174
+ .returning({ id: reg.sharesTable.id });
175
+ if (inserted) {
176
+ return { ...result, id, created: true, updated: false, role: input.role };
177
+ }
178
+ const existingAfterConflict = await findExisting();
179
+ if (!existingAfterConflict) {
180
+ throw new Error("Share conflict could not be resolved.");
181
+ }
182
+ return {
183
+ ...result,
184
+ ...(await setRole(existingAfterConflict)),
185
+ created: false,
186
+ };
187
+ }
188
+ /**
189
+ * Tells open sessions that a resource's sharing changed, after the grant that
190
+ * changed it has committed.
191
+ */
192
+ export async function announceResourceAccessChange(resourceType, resourceId, extensionTargetsBefore) {
193
+ invalidateCollabAccessCache(resourceType, resourceId);
194
+ await notifyExtensionShareChanged(resourceType, resourceId, extensionTargetsBefore);
195
+ }
@@ -1,4 +1,5 @@
1
1
  export { ownableColumns, createSharesTable, roleSatisfies, ROLE_RANK, type Visibility, type ShareRole, type PrincipalType, } from "./schema.js";
2
2
  export { registerShareableResource, getShareableResource, requireShareableResource, listShareableResources, type ShareableResourceRegistration, type ShareEmailExtras, } from "./registry.js";
3
- export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, type AccessContext, type ResolvedAccess, } from "./access.js";
3
+ export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, type AccessContext, type ResolvedAccess, type ResourceAccessState, type ResourceAccessStatus, } from "./access.js";
4
+ export { ACCESS_REQUEST_NOTE_MAX_LENGTH, accessRequestReviewPath, approveAccessRequest, declineAccessRequest, getAccessRequestReview, listResourceAccessRequests, requestResourceAccess, resolveLinkStatus, type AccessGrantedEmail, type AccessRequestReview, type RequestResourceAccessResult, type ResourceAccessRequestList, type ResourceLinkStatus, type ViewerAccessRequest, } from "./access-requests.js";
4
5
  export { filterRecipientsByResourceAccess, type FilterRecipientsInput, } from "./recipients.js";
@@ -1,4 +1,5 @@
1
1
  export { ownableColumns, createSharesTable, roleSatisfies, ROLE_RANK, } from "./schema.js";
2
2
  export { registerShareableResource, getShareableResource, requireShareableResource, listShareableResources, } from "./registry.js";
3
- export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, } from "./access.js";
3
+ export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, } from "./access.js";
4
+ export { ACCESS_REQUEST_NOTE_MAX_LENGTH, accessRequestReviewPath, approveAccessRequest, declineAccessRequest, getAccessRequestReview, listResourceAccessRequests, requestResourceAccess, resolveLinkStatus, } from "./access-requests.js";
4
5
  export { filterRecipientsByResourceAccess, } from "./recipients.js";