@agent-native/core 0.224.0-nightly-20261104194955 → 0.224.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/framework-package-versions.json +11 -11
- package/dist/mcp/analytics.js +3 -3
- package/dist/mcp/direct-action-call.js +2 -2
- package/dist/mcp/embed-redaction.d.ts +8 -0
- package/dist/mcp/embed-redaction.js +21 -0
- package/dist/mcp/execute-action-call.js +2 -2
- package/dist/server/action-routes.js +3 -1
- package/dist/sharing/access.d.ts +14 -0
- package/dist/sharing/access.js +131 -42
- package/dist/sharing/index.d.ts +1 -1
- package/dist/sharing/index.js +1 -1
- package/dist/sharing/registry.d.ts +11 -0
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/default/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/headless/.agents/skills/sharing/SKILL.md +4 -0
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@agent-native/agentkit": "0.224.0
|
|
2
|
+
"@agent-native/agentkit": "0.224.0",
|
|
3
3
|
"@agent-native/code-agents-ui": "0.1.75",
|
|
4
|
-
"@agent-native/core": "0.224.0
|
|
5
|
-
"@agent-native/core-corpus": "0.224.0
|
|
6
|
-
"@agent-native/creative-context": "0.8.
|
|
7
|
-
"@agent-native/dispatch": "0.42.0
|
|
4
|
+
"@agent-native/core": "0.224.0",
|
|
5
|
+
"@agent-native/core-corpus": "0.224.0",
|
|
6
|
+
"@agent-native/creative-context": "0.8.65",
|
|
7
|
+
"@agent-native/dispatch": "0.42.0",
|
|
8
8
|
"@agent-native/embedding": "0.2.0",
|
|
9
9
|
"@agent-native/migrate": "0.0.7",
|
|
10
|
-
"@agent-native/otel": "0.1.
|
|
11
|
-
"@agent-native/pinpoint": "0.1.
|
|
12
|
-
"@agent-native/recap-cli": "0.5.
|
|
13
|
-
"@agent-native/scheduling": "0.2.
|
|
10
|
+
"@agent-native/otel": "0.1.32",
|
|
11
|
+
"@agent-native/pinpoint": "0.1.108",
|
|
12
|
+
"@agent-native/recap-cli": "0.5.96",
|
|
13
|
+
"@agent-native/scheduling": "0.2.52",
|
|
14
14
|
"@agent-native/shared-app-config": "0.1.2",
|
|
15
|
-
"@agent-native/skills": "0.4.0
|
|
16
|
-
"@agent-native/toolkit": "0.224.0
|
|
15
|
+
"@agent-native/skills": "0.4.0",
|
|
16
|
+
"@agent-native/toolkit": "0.224.0"
|
|
17
17
|
}
|
package/dist/mcp/analytics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAppConfig } from "../app-config/store.js";
|
|
2
2
|
import { safeValue } from "../tracking/redaction.js";
|
|
3
3
|
import { listTrackingProviders, track } from "../tracking/registry.js";
|
|
4
|
-
import {
|
|
4
|
+
import { databaseFailureCode, databaseFailureMessage, isDatabaseQueryFailure, } from "./embed-redaction.js";
|
|
5
5
|
const CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo";
|
|
6
6
|
const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
7
7
|
export const MCP_ANALYTICS_EVENTS = {
|
|
@@ -206,8 +206,8 @@ export function trackMcpOAuthLoginWallShown(ctx, args) {
|
|
|
206
206
|
export function describeMcpError(err) {
|
|
207
207
|
if (isDatabaseQueryFailure(err)) {
|
|
208
208
|
return {
|
|
209
|
-
errorType: "DrizzleQueryError",
|
|
210
|
-
errorMessage:
|
|
209
|
+
errorType: databaseFailureCode(err) ?? "DrizzleQueryError",
|
|
210
|
+
errorMessage: databaseFailureMessage(err),
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
if (err && typeof err === "object") {
|
|
@@ -8,7 +8,7 @@ import { getRequestContext, getRequestOrgId, getRequestUserEmail, } from "../ser
|
|
|
8
8
|
import { describeMcpError } from "./analytics.js";
|
|
9
9
|
import { actionApprovalError, actionApprovalReviewResult, buildLinkArtifacts, conciseMcpAppToolText, conciseToolResultText, getMcpDirectoryWidgetResourceUri, hasVerifiedMcpUserIdentity, MCP_ACTION_APPROVAL_INPUT_KEY, mcpAppEmbedOpenLinkMeta, mcpAppOnlyEmbedStartMeta, mcpAppStructuredContent, mcpAppWidgetsEnabledForIdentity, mcpDirectoryWidgetCapabilityForTool, mcpDirectoryWidgetSourceMeta, mcpResponseStructuredContent, mcpResultHasContent, metadataObject, modelVisibleToolResponse, openAiToolResultMeta, PRESERVE_MCP_OBJECT_RESULT, resolveMcpAppResourceSafely, returnsQueryPayload, textPagingInputs, upstreamStatusText, withheldWriteResponse, withoutMcpAppEmbedTicket, withServerMintedMcpAppEmbedStart, } from "./build-server.js";
|
|
10
10
|
import { mcpCallAttribution } from "./call-attribution.js";
|
|
11
|
-
import {
|
|
11
|
+
import { databaseFailureMessage, describeThrownForLog, HIDDEN_EMBED_URL, isDatabaseQueryFailure, isEmbedStartUrl, redactEmbedText, } from "./embed-redaction.js";
|
|
12
12
|
import { embedCredentialSources, mcpActionErrorCode, redactEmbedCredentials, returnedResult, runMcpActionCallUnredacted, toJsonData, UnreadableResultError, } from "./execute-action-call.js";
|
|
13
13
|
import { mcpResponseOutputCheck } from "./output-contract.js";
|
|
14
14
|
export function createDirectActionCall(scope) {
|
|
@@ -45,7 +45,7 @@ export function createDirectActionCall(scope) {
|
|
|
45
45
|
: callableActions[name]?.mcpOutputContract;
|
|
46
46
|
const errorText = (err) => {
|
|
47
47
|
if (isDatabaseQueryFailure(err))
|
|
48
|
-
return
|
|
48
|
+
return databaseFailureMessage(err);
|
|
49
49
|
const projectedError = directoryCatalog && config.directoryProfile?.projectResult
|
|
50
50
|
? config.directoryProfile.projectResult(name, err.message)
|
|
51
51
|
: err.message;
|
|
@@ -13,6 +13,14 @@ export declare const HIDDEN_EMBED_URL = "[hidden embed URL]";
|
|
|
13
13
|
export declare const DATABASE_FAILURE_MESSAGE = "The database rejected the request.";
|
|
14
14
|
/** Drizzle puts a failed query's SQL and bound parameters in its message. */
|
|
15
15
|
export declare function isDatabaseQueryFailure(error: unknown): boolean;
|
|
16
|
+
export declare function databaseFailureCode(error: unknown): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The client-safe text for a failure `isDatabaseQueryFailure` recognizes. It
|
|
19
|
+
* names the code without saying what it means, because one code has several
|
|
20
|
+
* causes: 57014 is any cancellation, and CONNECT_TIMEOUT is `withDbTimeout` or
|
|
21
|
+
* the driver's wait for a connection.
|
|
22
|
+
*/
|
|
23
|
+
export declare function databaseFailureMessage(error: unknown): string;
|
|
16
24
|
/** Replaces a value already being serialized higher up the same result. */
|
|
17
25
|
export declare const CIRCULAR_RESULT = "[circular result]";
|
|
18
26
|
/** Keys removed from every result object, whatever their value. */
|
|
@@ -16,6 +16,27 @@ export const DATABASE_FAILURE_MESSAGE = "The database rejected the request.";
|
|
|
16
16
|
export function isDatabaseQueryFailure(error) {
|
|
17
17
|
return error instanceof Error && error.message.startsWith("Failed query:");
|
|
18
18
|
}
|
|
19
|
+
// A driver's error code names the failure without any of the query's values.
|
|
20
|
+
// Anything else in that field stays hidden along with the message.
|
|
21
|
+
const DATABASE_ERROR_CODE = /^[A-Z0-9_]{1,40}$/;
|
|
22
|
+
export function databaseFailureCode(error) {
|
|
23
|
+
const code = error.cause?.code;
|
|
24
|
+
return typeof code === "string" && DATABASE_ERROR_CODE.test(code)
|
|
25
|
+
? code
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The client-safe text for a failure `isDatabaseQueryFailure` recognizes. It
|
|
30
|
+
* names the code without saying what it means, because one code has several
|
|
31
|
+
* causes: 57014 is any cancellation, and CONNECT_TIMEOUT is `withDbTimeout` or
|
|
32
|
+
* the driver's wait for a connection.
|
|
33
|
+
*/
|
|
34
|
+
export function databaseFailureMessage(error) {
|
|
35
|
+
const code = databaseFailureCode(error);
|
|
36
|
+
return code
|
|
37
|
+
? `The database request failed (${code}).`
|
|
38
|
+
: DATABASE_FAILURE_MESSAGE;
|
|
39
|
+
}
|
|
19
40
|
/** Replaces a value already being serialized higher up the same result. */
|
|
20
41
|
export const CIRCULAR_RESULT = "[circular result]";
|
|
21
42
|
/** Keys removed from every result object, whatever their value. */
|
|
@@ -6,7 +6,7 @@ import { MCP_ACTION_RESULT_MARKER, isMcpActionResult, mcpActionResultText, } fro
|
|
|
6
6
|
import { assertServicePrincipalMayCall, assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
|
|
7
7
|
import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
|
|
8
8
|
import { answerMcpApprovalPrompt, consumeMcpApprovalGrant, consumeReviewedMcpApprovalGrant, createMcpApprovalGrant, findRecentMcpApprovalRun, storedApprovalArguments, recordMcpApprovalRun, } from "./approval-store.js";
|
|
9
|
-
import { CIRCULAR_RESULT, EMBED_RESULT_SENSITIVE_KEYS,
|
|
9
|
+
import { CIRCULAR_RESULT, EMBED_RESULT_SENSITIVE_KEYS, databaseFailureMessage, HIDDEN_EMBED_URL, IMAGE_BYTES, isDatabaseQueryFailure, isEmbedCredentialKey, readString, redactEmbedText, } from "./embed-redaction.js";
|
|
10
10
|
import { hasMcpOAuthScope } from "./oauth-token.js";
|
|
11
11
|
/**
|
|
12
12
|
* The authorization, approval, execution and result-sanitizing steps of an
|
|
@@ -504,7 +504,7 @@ function failedOutcome(stage, error) {
|
|
|
504
504
|
let errorCode;
|
|
505
505
|
try {
|
|
506
506
|
const raw = isDatabaseQueryFailure(error)
|
|
507
|
-
?
|
|
507
|
+
? databaseFailureMessage(error)
|
|
508
508
|
: error instanceof Error
|
|
509
509
|
? error.message
|
|
510
510
|
: error;
|
|
@@ -1447,9 +1447,11 @@ function webMcpManifestEtag(manifest) {
|
|
|
1447
1447
|
function ifNoneMatchIncludes(header, etag) {
|
|
1448
1448
|
if (!header)
|
|
1449
1449
|
return false;
|
|
1450
|
+
// A compressed response reaches the browser with "-df" on its tag (seen on
|
|
1451
|
+
// our Netlify hosts), and the browser sends that tag back as it received it.
|
|
1450
1452
|
return header
|
|
1451
1453
|
.split(",")
|
|
1452
|
-
.some((tag) => tag.trim().replace(/^W\//, "") === etag);
|
|
1454
|
+
.some((tag) => tag.trim().replace(/^W\//, "").replace(/-df"$/, '"') === etag);
|
|
1453
1455
|
}
|
|
1454
1456
|
export function mountWebMcpActionRoutes(nitroApp, actions, options) {
|
|
1455
1457
|
const eligible = Object.fromEntries(Object.entries(actions).filter(([name, entry]) => /^[A-Za-z0-9_.-]{1,128}$/.test(name) &&
|
package/dist/sharing/access.d.ts
CHANGED
|
@@ -56,6 +56,20 @@ export declare function resolveAccess(resourceType: string, resourceId: string,
|
|
|
56
56
|
export declare function resolveAccess(resourceType: string, resourceId: string, rawCtx: AccessContext | undefined, options: {
|
|
57
57
|
skipResourceBody: true;
|
|
58
58
|
}): Promise<ResolvedAccessProjected | null>;
|
|
59
|
+
/**
|
|
60
|
+
* {@link resolveAccess} for a list of resources of one type: the ids the
|
|
61
|
+
* viewer can open, mapped to their access. Each decision is the same one
|
|
62
|
+
* `resolveAccess` makes. What changes is the reads: one query loads the rows,
|
|
63
|
+
* one loads their shares, and org membership, group membership, and a
|
|
64
|
+
* registration's `canManageAccessKey` answers are looked up once, not once per
|
|
65
|
+
* resource. Ids that do not exist or that the viewer cannot open are left out.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveAccessMany(resourceType: string, resourceIds: string[], rawCtx?: AccessContext, options?: {
|
|
68
|
+
skipResourceBody?: false;
|
|
69
|
+
}): Promise<Map<string, ResolvedAccess>>;
|
|
70
|
+
export declare function resolveAccessMany(resourceType: string, resourceIds: string[], rawCtx: AccessContext | undefined, options: {
|
|
71
|
+
skipResourceBody: true;
|
|
72
|
+
}): Promise<Map<string, ResolvedAccessProjected>>;
|
|
59
73
|
/**
|
|
60
74
|
* Host hook: for resources that live inside another product (a design bound
|
|
61
75
|
* to a Builder project), the host is the authority on who may open them. The
|
package/dist/sharing/access.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq, isNull, or, sql } from "drizzle-orm";
|
|
1
|
+
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
2
2
|
import { drizzle as drizzleProxy } from "drizzle-orm/pg-proxy";
|
|
3
3
|
import { getScopedDbExec, withDbExec } from "../db/client.js";
|
|
4
4
|
import { evaluateFeatureFlagStrict } from "../feature-flags/store.js";
|
|
@@ -389,7 +389,7 @@ export function isResourceAvailable(reg, resource) {
|
|
|
389
389
|
function hasDynamicPublicAccessRoleResolver(reg) {
|
|
390
390
|
return typeof reg.publicAccessRole === "function";
|
|
391
391
|
}
|
|
392
|
-
async function
|
|
392
|
+
async function loadRowsForAccess(reg, where, options = {}) {
|
|
393
393
|
const db = reg.getDb();
|
|
394
394
|
// Hooks that receive the row may read any column, so they get all of it.
|
|
395
395
|
const useProjection = options.skipResourceBody === true &&
|
|
@@ -402,10 +402,7 @@ async function loadResourceForAccess(reg, resourceId, options = {}) {
|
|
|
402
402
|
const query = !projectedColumns && omittedColumnNames.size === 0
|
|
403
403
|
? db.select()
|
|
404
404
|
: db.select(selectExistingColumns(projectedColumns ?? reg.resourceTable, omittedColumnNames));
|
|
405
|
-
|
|
406
|
-
.from(reg.resourceTable)
|
|
407
|
-
.where(eq(reg.resourceTable.id, resourceId));
|
|
408
|
-
return resource ?? null;
|
|
405
|
+
return await query.from(reg.resourceTable).where(where);
|
|
409
406
|
}
|
|
410
407
|
catch (err) {
|
|
411
408
|
const missing = missingColumnName(err);
|
|
@@ -415,7 +412,7 @@ async function loadResourceForAccess(reg, resourceId, options = {}) {
|
|
|
415
412
|
console.warn(`[sharing] ${reg.type} access lookup omitted missing column ${missing}`);
|
|
416
413
|
}
|
|
417
414
|
}
|
|
418
|
-
throw new Error(`Could not load ${reg.type}
|
|
415
|
+
throw new Error(`Could not load ${reg.type} rows: too many missing resource columns`);
|
|
419
416
|
}
|
|
420
417
|
export async function resolveAccess(resourceType, resourceId, rawCtx = currentAccess(), options = {}) {
|
|
421
418
|
const { access } = await inAccessTransaction(rawCtx, () => loadAndResolveAccess(resourceType, resourceId, rawCtx, options));
|
|
@@ -424,7 +421,7 @@ export async function resolveAccess(resourceType, resourceId, rawCtx = currentAc
|
|
|
424
421
|
function inAccessTransaction(ctx, run) {
|
|
425
422
|
return ctx.transaction ? withDbExec(ctx.transaction, run) : run();
|
|
426
423
|
}
|
|
427
|
-
|
|
424
|
+
function scopeAccess(resourceType, rawCtx) {
|
|
428
425
|
const registered = requireShareableResource(resourceType);
|
|
429
426
|
const transaction = rawCtx.transaction ?? getScopedDbExec();
|
|
430
427
|
const transactionDb = transaction
|
|
@@ -437,8 +434,11 @@ async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
|
|
|
437
434
|
const reg = transactionDb
|
|
438
435
|
? { ...registered, getDb: () => transactionDb }
|
|
439
436
|
: registered;
|
|
440
|
-
|
|
441
|
-
|
|
437
|
+
return { reg, ctx: resolveRegisteredAccessContext(reg, transactionCtx) };
|
|
438
|
+
}
|
|
439
|
+
async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
|
|
440
|
+
const { reg, ctx } = scopeAccess(resourceType, rawCtx);
|
|
441
|
+
const [resource] = await loadRowsForAccess(reg, eq(reg.resourceTable.id, resourceId), options);
|
|
442
442
|
if (!resource)
|
|
443
443
|
return { access: null, resource: null, reg };
|
|
444
444
|
return {
|
|
@@ -447,6 +447,76 @@ async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
|
|
|
447
447
|
reg,
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
|
+
const RESOLVE_MANY_ROW_CHUNK = 500;
|
|
451
|
+
const RESOLVE_MANY_CONCURRENCY = 8;
|
|
452
|
+
export async function resolveAccessMany(resourceType, resourceIds, rawCtx = currentAccess(), options = {}) {
|
|
453
|
+
return inAccessTransaction(rawCtx, async () => {
|
|
454
|
+
const { reg, ctx } = scopeAccess(resourceType, rawCtx);
|
|
455
|
+
const ids = [...new Set(resourceIds)];
|
|
456
|
+
const resources = [];
|
|
457
|
+
for (let start = 0; start < ids.length; start += RESOLVE_MANY_ROW_CHUNK) {
|
|
458
|
+
resources.push(...(await loadRowsForAccess(reg, inArray(reg.resourceTable.id, ids.slice(start, start + RESOLVE_MANY_ROW_CHUNK)), options)));
|
|
459
|
+
}
|
|
460
|
+
const batch = {
|
|
461
|
+
memberships: new Map(),
|
|
462
|
+
groupMemberships: new Map(),
|
|
463
|
+
managers: new Map(),
|
|
464
|
+
shares: await loadSharesForAccess(reg, resources.map((resource) => resource.id), ctx),
|
|
465
|
+
};
|
|
466
|
+
const access = new Map();
|
|
467
|
+
for (let start = 0; start < resources.length; start += RESOLVE_MANY_CONCURRENCY) {
|
|
468
|
+
await Promise.all(resources
|
|
469
|
+
.slice(start, start + RESOLVE_MANY_CONCURRENCY)
|
|
470
|
+
.map(async (resource) => {
|
|
471
|
+
const resolved = await accessToResource(reg, resource.id, resource, ctx, batch);
|
|
472
|
+
if (resolved)
|
|
473
|
+
access.set(resource.id, resolved);
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
return access;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The grants {@link highestShareRole} reads, for many resources at once: the
|
|
481
|
+
* viewer's own user grants, their org's grants, and every group grant.
|
|
482
|
+
*/
|
|
483
|
+
async function loadSharesForAccess(reg, resourceIds, ctx) {
|
|
484
|
+
const byResource = new Map();
|
|
485
|
+
const normalizedUserEmail = normalizeEmailForAccess(ctx.userEmail);
|
|
486
|
+
const principalClauses = principalShareClauses(reg, ctx);
|
|
487
|
+
if (reg.supportsGroupShares && normalizedUserEmail) {
|
|
488
|
+
principalClauses.push(eq(reg.sharesTable.principalType, "group"));
|
|
489
|
+
}
|
|
490
|
+
if (resourceIds.length === 0 || principalClauses.length === 0) {
|
|
491
|
+
return byResource;
|
|
492
|
+
}
|
|
493
|
+
const db = reg.getDb();
|
|
494
|
+
for (let start = 0; start < resourceIds.length; start += RESOLVE_MANY_ROW_CHUNK) {
|
|
495
|
+
const rows = (await db
|
|
496
|
+
.select({
|
|
497
|
+
resourceId: reg.sharesTable.resourceId,
|
|
498
|
+
principalType: reg.sharesTable.principalType,
|
|
499
|
+
principalId: reg.sharesTable.principalId,
|
|
500
|
+
role: reg.sharesTable.role,
|
|
501
|
+
})
|
|
502
|
+
.from(reg.sharesTable)
|
|
503
|
+
.where(and(inArray(reg.sharesTable.resourceId, resourceIds.slice(start, start + RESOLVE_MANY_ROW_CHUNK)), or(...principalClauses))));
|
|
504
|
+
for (const row of rows) {
|
|
505
|
+
const list = byResource.get(row.resourceId) ?? [];
|
|
506
|
+
list.push(row);
|
|
507
|
+
byResource.set(row.resourceId, list);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return byResource;
|
|
511
|
+
}
|
|
512
|
+
function memoized(cache, key, load) {
|
|
513
|
+
let value = cache.get(key);
|
|
514
|
+
if (!value) {
|
|
515
|
+
value = load();
|
|
516
|
+
cache.set(key, value);
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
450
520
|
const hostAccessResolvers = new Map();
|
|
451
521
|
export function registerHostAccessResolver(resourceType, resolver, options) {
|
|
452
522
|
if (resolver) {
|
|
@@ -459,21 +529,27 @@ export function registerHostAccessResolver(resourceType, resolver, options) {
|
|
|
459
529
|
hostAccessResolvers.delete(resourceType);
|
|
460
530
|
}
|
|
461
531
|
}
|
|
462
|
-
async function ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail) {
|
|
532
|
+
async function ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail, batch) {
|
|
463
533
|
if (normalizedUserEmail &&
|
|
464
534
|
normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&
|
|
465
535
|
ownerMatchesActiveScope(reg, resource, ctx)) {
|
|
466
536
|
return "owner";
|
|
467
537
|
}
|
|
468
|
-
if (reg.canManageAccess
|
|
469
|
-
|
|
538
|
+
if (reg.canManageAccess) {
|
|
539
|
+
const manage = reg.canManageAccess;
|
|
540
|
+
const key = batch && reg.canManageAccessKey?.(resource, ctx);
|
|
541
|
+
const manages = batch && key != null
|
|
542
|
+
? await memoized(batch.managers, key, async () => Boolean(await manage(resource, ctx)))
|
|
543
|
+
: await manage(resource, ctx);
|
|
544
|
+
if (manages)
|
|
545
|
+
return "admin";
|
|
470
546
|
}
|
|
471
547
|
return undefined;
|
|
472
548
|
}
|
|
473
|
-
async function accessToResource(reg, resourceId, resource, ctx) {
|
|
549
|
+
async function accessToResource(reg, resourceId, resource, ctx, batch) {
|
|
474
550
|
const { userEmail } = ctx;
|
|
475
551
|
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
476
|
-
const memberships = new Map();
|
|
552
|
+
const memberships = batch?.memberships ?? new Map();
|
|
477
553
|
const isOrgMember = (orgId) => {
|
|
478
554
|
let membership = memberships.get(orgId);
|
|
479
555
|
if (!membership) {
|
|
@@ -492,12 +568,12 @@ async function accessToResource(reg, resourceId, resource, ctx) {
|
|
|
492
568
|
: undefined;
|
|
493
569
|
if (hosted === null)
|
|
494
570
|
return null;
|
|
495
|
-
const managingRole = await ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail);
|
|
571
|
+
const managingRole = await ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail, batch);
|
|
496
572
|
if (managingRole)
|
|
497
573
|
return { role: managingRole, resource };
|
|
498
574
|
if (hosted !== undefined)
|
|
499
575
|
return { role: hosted, resource };
|
|
500
|
-
const role = await highestShareRole(reg, resourceId, ctx, resource, isOrgMember);
|
|
576
|
+
const role = await highestShareRole(reg, resourceId, ctx, resource, isOrgMember, batch);
|
|
501
577
|
const shareRole = role === "admin" && reg.canManageAccess ? "editor" : role;
|
|
502
578
|
if (resource.visibility === "public" && reg.allowPublic !== false) {
|
|
503
579
|
const publicRole = await publicAccessRoleForResource(reg, resource, ctx);
|
|
@@ -545,7 +621,18 @@ export async function resolveAccessStatus(resourceType, resourceId, ctx = curren
|
|
|
545
621
|
return { state: available ? "allowed" : "trashed", role: access.role };
|
|
546
622
|
});
|
|
547
623
|
}
|
|
548
|
-
|
|
624
|
+
function principalShareClauses(reg, ctx) {
|
|
625
|
+
const normalizedUserEmail = normalizeEmailForAccess(ctx.userEmail);
|
|
626
|
+
const clauses = [];
|
|
627
|
+
if (normalizedUserEmail) {
|
|
628
|
+
clauses.push(and(eq(reg.sharesTable.principalType, "user"), emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail)));
|
|
629
|
+
}
|
|
630
|
+
if (ctx.orgId) {
|
|
631
|
+
clauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, ctx.orgId)));
|
|
632
|
+
}
|
|
633
|
+
return clauses;
|
|
634
|
+
}
|
|
635
|
+
async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember, batch) {
|
|
549
636
|
const { userEmail, orgId } = ctx;
|
|
550
637
|
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
551
638
|
if (!normalizedUserEmail && !orgId)
|
|
@@ -553,23 +640,20 @@ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
|
|
|
553
640
|
if (!explicitSharesAllowedForResource(reg, resource, ctx))
|
|
554
641
|
return null;
|
|
555
642
|
const db = reg.getDb();
|
|
556
|
-
const principalClauses =
|
|
557
|
-
if (normalizedUserEmail) {
|
|
558
|
-
principalClauses.push(and(eq(reg.sharesTable.principalType, "user"), emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail)));
|
|
559
|
-
}
|
|
560
|
-
if (orgId) {
|
|
561
|
-
principalClauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, orgId)));
|
|
562
|
-
}
|
|
643
|
+
const principalClauses = principalShareClauses(reg, ctx);
|
|
563
644
|
if (principalClauses.length === 0)
|
|
564
645
|
return null;
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
646
|
+
const prefetched = batch?.shares.get(resourceId) ?? [];
|
|
647
|
+
const rows = batch
|
|
648
|
+
? prefetched.filter((row) => row.principalType !== "group")
|
|
649
|
+
: await db
|
|
650
|
+
.select({
|
|
651
|
+
role: reg.sharesTable.role,
|
|
652
|
+
principalType: reg.sharesTable.principalType,
|
|
653
|
+
})
|
|
654
|
+
.from(reg.sharesTable)
|
|
655
|
+
.where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
|
|
656
|
+
.limit(10);
|
|
573
657
|
let best = null;
|
|
574
658
|
const grantRows = rows;
|
|
575
659
|
for (const row of grantRows) {
|
|
@@ -588,18 +672,23 @@ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
|
|
|
588
672
|
}
|
|
589
673
|
}
|
|
590
674
|
if (reg.supportsGroupShares && normalizedUserEmail && resource.orgId) {
|
|
591
|
-
const groupRows =
|
|
592
|
-
.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
675
|
+
const groupRows = batch
|
|
676
|
+
? prefetched.filter((row) => row.principalType === "group")
|
|
677
|
+
: await db
|
|
678
|
+
.select({
|
|
679
|
+
principalId: reg.sharesTable.principalId,
|
|
680
|
+
role: reg.sharesTable.role,
|
|
681
|
+
})
|
|
682
|
+
.from(reg.sharesTable)
|
|
683
|
+
.where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
|
|
598
684
|
if (groupRows.length > 0 && (await isOrgMember(resource.orgId))) {
|
|
599
685
|
for (const row of groupRows) {
|
|
600
|
-
|
|
686
|
+
const includesUser = () => workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail);
|
|
687
|
+
const member = batch
|
|
688
|
+
? await memoized(batch.groupMemberships, `${resource.orgId}\u0000${row.principalId}`, includesUser)
|
|
689
|
+
: await includesUser();
|
|
690
|
+
if (member)
|
|
601
691
|
best = higherShareRole(row.role, best);
|
|
602
|
-
}
|
|
603
692
|
}
|
|
604
693
|
}
|
|
605
694
|
}
|
package/dist/sharing/index.d.ts
CHANGED
|
@@ -1,5 +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, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, type HostAccessResolver, type AccessContext, type ResolvedAccess, type ResourceAccessState, type ResourceAccessStatus, } from "./access.js";
|
|
3
|
+
export { accessFilter, resolveAccess, resolveAccessMany, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, type HostAccessResolver, type AccessContext, type ResolvedAccess, type ResourceAccessState, type ResourceAccessStatus, } from "./access.js";
|
|
4
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";
|
|
5
5
|
export { filterRecipientsByResourceAccess, type FilterRecipientsInput, } from "./recipients.js";
|
package/dist/sharing/index.js
CHANGED
|
@@ -1,5 +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, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, } from "./access.js";
|
|
3
|
+
export { accessFilter, resolveAccess, resolveAccessMany, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, } from "./access.js";
|
|
4
4
|
export { ACCESS_REQUEST_NOTE_MAX_LENGTH, accessRequestReviewPath, approveAccessRequest, declineAccessRequest, getAccessRequestReview, listResourceAccessRequests, requestResourceAccess, resolveLinkStatus, } from "./access-requests.js";
|
|
5
5
|
export { filterRecipientsByResourceAccess, } from "./recipients.js";
|
|
@@ -86,6 +86,17 @@ export interface ShareableResourceRegistration {
|
|
|
86
86
|
userEmail?: string;
|
|
87
87
|
orgId?: string;
|
|
88
88
|
}) => boolean | Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Resources that return the same key get the same `canManageAccess` answer
|
|
91
|
+
* for the same viewer, so `resolveAccessMany` asks once per key. Return
|
|
92
|
+
* `undefined` for a resource whose answer depends on more than the key.
|
|
93
|
+
*/
|
|
94
|
+
canManageAccessKey?: (resource: {
|
|
95
|
+
orgId?: string | null;
|
|
96
|
+
}, ctx: {
|
|
97
|
+
userEmail?: string;
|
|
98
|
+
orgId?: string;
|
|
99
|
+
}) => string | undefined;
|
|
89
100
|
resolveAccessContext?: (ctx: {
|
|
90
101
|
userEmail?: string;
|
|
91
102
|
orgId?: string;
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.224.0
|
|
3
|
+
"version": "0.224.0",
|
|
4
4
|
"description": "The agentic application framework for building autonomous agents with intuitive UIs",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -760,8 +760,8 @@
|
|
|
760
760
|
"yaml": "^2.9.0",
|
|
761
761
|
"yjs": "^13.6.33",
|
|
762
762
|
"zod": "^4.3.6",
|
|
763
|
-
"@agent-native/
|
|
764
|
-
"@agent-native/
|
|
763
|
+
"@agent-native/recap-cli": "^0.5.96",
|
|
764
|
+
"@agent-native/agentkit": "^0.224.0"
|
|
765
765
|
},
|
|
766
766
|
"devDependencies": {
|
|
767
767
|
"@ai-sdk/anthropic": "^3.0.71",
|
|
@@ -833,7 +833,7 @@
|
|
|
833
833
|
"xlsx": "^0.18.5"
|
|
834
834
|
},
|
|
835
835
|
"peerDependencies": {
|
|
836
|
-
"@agent-native/core-corpus": "0.
|
|
836
|
+
"@agent-native/core-corpus": ">=0.196.0",
|
|
837
837
|
"@ai-sdk/anthropic": ">=3",
|
|
838
838
|
"@ai-sdk/cohere": ">=3",
|
|
839
839
|
"@ai-sdk/google": ">=3",
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|
|
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
|
|
|
174
174
|
|
|
175
175
|
`authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
|
|
176
176
|
|
|
177
|
+
## Check many resources at once
|
|
178
|
+
|
|
179
|
+
Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
|
|
180
|
+
|
|
177
181
|
## Create actions must set owner
|
|
178
182
|
|
|
179
183
|
When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
|