@agent-native/core 0.198.3 → 0.198.4
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/a2a/types.d.ts +5 -0
- package/dist/agent/production-agent.js +26 -1
- package/dist/automation/index.d.ts +1 -1
- package/dist/catalog.json +1 -0
- package/dist/client/chat/agentkit-agent-native.js +15 -15
- package/dist/client/chat/runtime.js +5 -3
- package/dist/client/feature-flags/types.d.ts +2 -0
- package/dist/client/i18n.d.ts +2 -0
- package/dist/client/i18n.js +2 -0
- package/dist/client/integrations/index.d.ts +1 -0
- package/dist/client/integrations/index.js +1 -0
- package/dist/client/labs/index.d.ts +1 -1
- package/dist/client/labs/use-lab.d.ts +5 -0
- package/dist/client/labs/use-lab.js +7 -3
- package/dist/client/resources/mcp-connection-resume.d.ts +5 -3
- package/dist/client/resources/mcp-connection-resume.js +150 -13
- package/dist/client/sse-event-processor.d.ts +6 -0
- package/dist/client/uploads/index.d.ts +1 -0
- package/dist/client/uploads/index.js +1 -0
- package/dist/client/uploads/private-icon.d.ts +10 -0
- package/dist/client/uploads/private-icon.js +52 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +2 -2
- package/dist/feature-flags/actions/get-feature-flags.js +20 -0
- package/dist/feature-flags/actions/list-feature-flags.d.ts +2 -0
- package/dist/feature-flags/actions/list-feature-flags.js +5 -0
- package/dist/feature-flags/actions/set-feature-flag.js +5 -0
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/framework-tools.js +2 -0
- package/dist/icon-assets/index.d.ts +3 -0
- package/dist/icon-assets/index.js +3 -0
- package/dist/icon-assets/multipart.d.ts +9 -0
- package/dist/icon-assets/multipart.js +60 -0
- package/dist/icon-assets/schema.d.ts +194 -0
- package/dist/icon-assets/schema.js +16 -0
- package/dist/icon-assets/store.d.ts +38 -0
- package/dist/icon-assets/store.js +187 -0
- package/dist/icon-assets/svg.d.ts +2 -0
- package/dist/icon-assets/svg.js +146 -0
- package/dist/icon-assets/workspace-transport.d.ts +20 -0
- package/dist/icon-assets/workspace-transport.js +122 -0
- package/dist/identity/rekey.js +1 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/labs/actions/get-lab-states.d.ts +2 -0
- package/dist/labs/actions/get-lab-states.js +14 -0
- package/dist/labs/actions/get-labs.js +2 -2
- package/dist/labs/actions/set-lab.js +4 -2
- package/dist/labs/index.d.ts +1 -1
- package/dist/labs/index.js +1 -1
- package/dist/labs/registry.d.ts +3 -0
- package/dist/labs/registry.js +14 -0
- package/dist/labs/server.d.ts +1 -1
- package/dist/labs/server.js +1 -1
- package/dist/labs/store.d.ts +13 -2
- package/dist/labs/store.js +124 -7
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +1 -1
- package/dist/localization/icon-picker-messages.d.ts +44 -0
- package/dist/localization/icon-picker-messages.js +22 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +42 -4
- package/dist/mcp-client/oauth-routes.js +15 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +9 -9
- package/dist/org/actions/list-workspace-icons.d.ts +7 -0
- package/dist/org/actions/list-workspace-icons.js +41 -0
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/handlers.js +28 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/org/migrations.js +21 -0
- package/dist/org/plugin.js +42 -0
- package/dist/org/private-icon-handlers.d.ts +6 -0
- package/dist/org/private-icon-handlers.js +212 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +1 -1
- package/dist/provider-api/index.js +26 -6
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/agent-chat-plugin.js +14 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +18 -3
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +1 -0
- package/dist/server/workspace-provider-oauth.js +4 -2
- package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -16
- package/dist/triggers/actions/manage-automation.d.ts +5 -5
- package/dist/vite/action-types-plugin.js +4 -0
- package/package.json +7 -9
- package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -16
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getLabForLegacyFlag } from "../../labs/registry.js";
|
|
4
|
+
import { getUserLabStates } from "../../labs/store.js";
|
|
3
5
|
import { captureError } from "../../server/capture-error.js";
|
|
4
6
|
import { listFeatureFlags } from "../registry.js";
|
|
5
7
|
import { defaultFeatureFlagRules, evaluateFeatureFlagRules, getFeatureFlagRules, getFeatureFlagRulesForKeys, } from "../store.js";
|
|
@@ -38,6 +40,24 @@ export default defineAction({
|
|
|
38
40
|
return [key, false];
|
|
39
41
|
}
|
|
40
42
|
}));
|
|
43
|
+
const migratedKeys = definitions
|
|
44
|
+
.map(({ key }) => key)
|
|
45
|
+
.filter((key) => getLabForLegacyFlag(key));
|
|
46
|
+
if (migratedKeys.length === 0 || !scope.userEmail)
|
|
47
|
+
return values;
|
|
48
|
+
const labStates = await getUserLabStates(scope.userEmail, scope);
|
|
49
|
+
for (const key of migratedKeys) {
|
|
50
|
+
const lab = getLabForLegacyFlag(key);
|
|
51
|
+
if (!lab)
|
|
52
|
+
continue;
|
|
53
|
+
const state = labStates[lab.key];
|
|
54
|
+
if (!state)
|
|
55
|
+
throw new Error(`Missing migrated lab state: ${lab.key}`);
|
|
56
|
+
values[key] =
|
|
57
|
+
state.source === "choice"
|
|
58
|
+
? state.enabled
|
|
59
|
+
: (state.legacyValues?.[key] ?? false);
|
|
60
|
+
}
|
|
41
61
|
return values;
|
|
42
62
|
},
|
|
43
63
|
});
|
|
@@ -19,6 +19,8 @@ declare const _default: import("../../action.js").ActionDefinition<Record<string
|
|
|
19
19
|
defaultValue?: false;
|
|
20
20
|
displayName?: string;
|
|
21
21
|
description?: string;
|
|
22
|
+
movedToLab: string;
|
|
23
|
+
canManage: boolean;
|
|
22
24
|
rules: import("../store.js").FeatureFlagRules;
|
|
23
25
|
enabledForCurrentUser: boolean;
|
|
24
26
|
}[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getLabForLegacyFlag } from "../../labs/registry.js";
|
|
3
4
|
import { requireFeatureFlagManager } from "../permissions.js";
|
|
4
5
|
import { listFeatureFlags } from "../registry.js";
|
|
5
6
|
import { evaluateFeatureFlagRules, getFeatureFlagRules } from "../store.js";
|
|
@@ -40,6 +41,10 @@ export default defineAction({
|
|
|
40
41
|
const rules = await getFeatureFlagRules(definition.key, manager);
|
|
41
42
|
return {
|
|
42
43
|
...definition,
|
|
44
|
+
...(getLabForLegacyFlag(definition.key) && {
|
|
45
|
+
movedToLab: getLabForLegacyFlag(definition.key)?.key,
|
|
46
|
+
canManage: false,
|
|
47
|
+
}),
|
|
43
48
|
rules,
|
|
44
49
|
enabledForCurrentUser: evaluateFeatureFlagRules(definition.key, rules, {
|
|
45
50
|
userEmail: manager.email,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getLabForLegacyFlag } from "../../labs/registry.js";
|
|
3
4
|
import { getOrgDomain } from "../../org/context.js";
|
|
4
5
|
import { requireFeatureFlagManager } from "../permissions.js";
|
|
5
6
|
import { getFeatureFlagDefinition } from "../registry.js";
|
|
@@ -45,6 +46,10 @@ export default defineAction({
|
|
|
45
46
|
if (!getFeatureFlagDefinition(args.key)) {
|
|
46
47
|
throw new Error(`Unknown feature flag: ${args.key}`);
|
|
47
48
|
}
|
|
49
|
+
const movedToLab = getLabForLegacyFlag(args.key);
|
|
50
|
+
if (movedToLab) {
|
|
51
|
+
throw new Error(`Feature flag ${args.key} is managed in Labs (${movedToLab.key}).`);
|
|
52
|
+
}
|
|
48
53
|
const orgDomain = manager.orgId
|
|
49
54
|
? (await getOrgDomain(manager.orgId))?.trim().toLowerCase() || null
|
|
50
55
|
: null;
|
|
@@ -33,9 +33,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
33
33
|
idempotencyKey: string;
|
|
34
34
|
deleted: boolean;
|
|
35
35
|
} | {
|
|
36
|
-
id?: undefined;
|
|
37
36
|
error: string;
|
|
38
37
|
url?: undefined;
|
|
38
|
+
id?: undefined;
|
|
39
39
|
provider?: undefined;
|
|
40
40
|
configured?: undefined;
|
|
41
41
|
} | {
|
|
@@ -45,8 +45,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
45
45
|
configured?: undefined;
|
|
46
46
|
error?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
id?: undefined;
|
|
49
48
|
url?: undefined;
|
|
49
|
+
id?: undefined;
|
|
50
50
|
provider?: undefined;
|
|
51
51
|
error: string;
|
|
52
52
|
configured: boolean;
|
package/dist/framework-tools.js
CHANGED
|
@@ -87,6 +87,7 @@ export const CORE_ACTION_GROUPS = {
|
|
|
87
87
|
"list-app-permissions": "orgAdministration",
|
|
88
88
|
"set-app-permission-roles": "orgAdministration",
|
|
89
89
|
"list-workspace-app-access": "orgAdministration",
|
|
90
|
+
"list-workspace-icons": "orgAdministration",
|
|
90
91
|
"set-workspace-app-access": "orgAdministration",
|
|
91
92
|
"list-sign-in-methods": "orgAdministration",
|
|
92
93
|
"explain-access": "orgAdministration",
|
|
@@ -107,6 +108,7 @@ export const CORE_ACTION_GROUPS = {
|
|
|
107
108
|
"set-feature-flag": "featureFlags",
|
|
108
109
|
"get-launchdarkly-flags": "launchDarkly",
|
|
109
110
|
"get-labs": "labs",
|
|
111
|
+
"get-lab-states": "labs",
|
|
110
112
|
"set-lab": "labs",
|
|
111
113
|
"get-chatgpt-subscription-status": "chat",
|
|
112
114
|
"disconnect-chatgpt-subscription": "chat",
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { sanitizeIconSvg } from "./svg.js";
|
|
2
|
+
export { getIconAsset, listIconAssets, putIconAsset, readIconAsset, readIconAssetForAuthorizedReference, type IconAsset, type IconAssetRead, type IconAssetScope, } from "./store.js";
|
|
3
|
+
export { IconUploadBodyError, MAX_ICON_MULTIPART_BYTES, readIconUploadFormData, } from "./multipart.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { sanitizeIconSvg } from "./svg.js";
|
|
2
|
+
export { getIconAsset, listIconAssets, putIconAsset, readIconAsset, readIconAssetForAuthorizedReference, } from "./store.js";
|
|
3
|
+
export { IconUploadBodyError, MAX_ICON_MULTIPART_BYTES, readIconUploadFormData, } from "./multipart.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const MAX_ICON_MULTIPART_BYTES: number;
|
|
2
|
+
export declare class IconUploadBodyError extends Error {
|
|
3
|
+
readonly statusCode: 400 | 413;
|
|
4
|
+
readonly cause?: unknown;
|
|
5
|
+
constructor(statusCode: 400 | 413, options?: {
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export declare function readIconUploadFormData(request: Request): Promise<FormData>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const MAX_ICON_MULTIPART_BYTES = 5 * 1024 * 1024 + 64 * 1024;
|
|
2
|
+
export class IconUploadBodyError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
constructor(statusCode, options) {
|
|
5
|
+
super(statusCode === 413 ? "Icon too large" : "Invalid multipart icon upload");
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.name = "IconUploadBodyError";
|
|
8
|
+
if (options && "cause" in options) {
|
|
9
|
+
Object.defineProperty(this, "cause", {
|
|
10
|
+
value: options.cause,
|
|
11
|
+
enumerable: false,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function rejectOversized(cancel) {
|
|
19
|
+
try {
|
|
20
|
+
await cancel();
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new IconUploadBodyError(413, { cause });
|
|
24
|
+
}
|
|
25
|
+
throw new IconUploadBodyError(413);
|
|
26
|
+
}
|
|
27
|
+
export async function readIconUploadFormData(request) {
|
|
28
|
+
const declaredLength = Number(request.headers.get("content-length"));
|
|
29
|
+
if (declaredLength > MAX_ICON_MULTIPART_BYTES) {
|
|
30
|
+
return rejectOversized(() => request.body?.cancel());
|
|
31
|
+
}
|
|
32
|
+
const reader = request.body?.getReader();
|
|
33
|
+
if (!reader)
|
|
34
|
+
throw new IconUploadBodyError(400);
|
|
35
|
+
const body = new Uint8Array(MAX_ICON_MULTIPART_BYTES);
|
|
36
|
+
let size = 0;
|
|
37
|
+
try {
|
|
38
|
+
while (true) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done)
|
|
41
|
+
break;
|
|
42
|
+
if (value.byteLength > MAX_ICON_MULTIPART_BYTES - size)
|
|
43
|
+
await rejectOversized(() => reader.cancel());
|
|
44
|
+
body.set(value, size);
|
|
45
|
+
size += value.byteLength;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
reader.releaseLock();
|
|
50
|
+
}
|
|
51
|
+
// Parse the bounded buffer, never a clone that tees and queues the live body.
|
|
52
|
+
try {
|
|
53
|
+
return await new Response(body.subarray(0, size), {
|
|
54
|
+
headers: { "content-type": request.headers.get("content-type") ?? "" },
|
|
55
|
+
}).formData();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new IconUploadBodyError(400);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export declare const iconAssets: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
2
|
+
name: "private_icon_assets";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "private_icon_assets";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgText";
|
|
10
|
+
data: string;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: false;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: [string, ...string[]];
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
ownerEmail: import("drizzle-orm/pg-core").PgColumn<{
|
|
23
|
+
name: "owner_email";
|
|
24
|
+
tableName: "private_icon_assets";
|
|
25
|
+
dataType: "string";
|
|
26
|
+
columnType: "PgText";
|
|
27
|
+
data: string;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: false;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: [string, ...string[]];
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
orgId: import("drizzle-orm/pg-core").PgColumn<{
|
|
40
|
+
name: "org_id";
|
|
41
|
+
tableName: "private_icon_assets";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgText";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: false;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: [string, ...string[]];
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
mimeType: import("drizzle-orm/pg-core").PgColumn<{
|
|
57
|
+
name: "mime_type";
|
|
58
|
+
tableName: "private_icon_assets";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgText";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: true;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: [string, ...string[]];
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
filename: import("drizzle-orm/pg-core").PgColumn<{
|
|
74
|
+
name: "filename";
|
|
75
|
+
tableName: "private_icon_assets";
|
|
76
|
+
dataType: "string";
|
|
77
|
+
columnType: "PgText";
|
|
78
|
+
data: string;
|
|
79
|
+
driverParam: string;
|
|
80
|
+
notNull: false;
|
|
81
|
+
hasDefault: false;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: [string, ...string[]];
|
|
86
|
+
baseColumn: never;
|
|
87
|
+
identity: undefined;
|
|
88
|
+
generated: undefined;
|
|
89
|
+
}, {}, {}>;
|
|
90
|
+
alt: import("drizzle-orm/pg-core").PgColumn<{
|
|
91
|
+
name: "alt";
|
|
92
|
+
tableName: "private_icon_assets";
|
|
93
|
+
dataType: "string";
|
|
94
|
+
columnType: "PgText";
|
|
95
|
+
data: string;
|
|
96
|
+
driverParam: string;
|
|
97
|
+
notNull: false;
|
|
98
|
+
hasDefault: false;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: [string, ...string[]];
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
size: import("drizzle-orm/pg-core").PgColumn<{
|
|
108
|
+
name: "size";
|
|
109
|
+
tableName: "private_icon_assets";
|
|
110
|
+
dataType: "number";
|
|
111
|
+
columnType: "PgBigInt53";
|
|
112
|
+
data: number;
|
|
113
|
+
driverParam: string | number;
|
|
114
|
+
notNull: true;
|
|
115
|
+
hasDefault: false;
|
|
116
|
+
isPrimaryKey: false;
|
|
117
|
+
isAutoincrement: false;
|
|
118
|
+
hasRuntimeDefault: false;
|
|
119
|
+
enumValues: undefined;
|
|
120
|
+
baseColumn: never;
|
|
121
|
+
identity: undefined;
|
|
122
|
+
generated: undefined;
|
|
123
|
+
}, {}, {}>;
|
|
124
|
+
sha256: import("drizzle-orm/pg-core").PgColumn<{
|
|
125
|
+
name: "sha256";
|
|
126
|
+
tableName: "private_icon_assets";
|
|
127
|
+
dataType: "string";
|
|
128
|
+
columnType: "PgText";
|
|
129
|
+
data: string;
|
|
130
|
+
driverParam: string;
|
|
131
|
+
notNull: true;
|
|
132
|
+
hasDefault: false;
|
|
133
|
+
isPrimaryKey: false;
|
|
134
|
+
isAutoincrement: false;
|
|
135
|
+
hasRuntimeDefault: false;
|
|
136
|
+
enumValues: [string, ...string[]];
|
|
137
|
+
baseColumn: never;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}, {}, {}>;
|
|
141
|
+
blobHandleJson: import("drizzle-orm/pg-core").PgColumn<{
|
|
142
|
+
name: "blob_handle_json";
|
|
143
|
+
tableName: "private_icon_assets";
|
|
144
|
+
dataType: "string";
|
|
145
|
+
columnType: "PgText";
|
|
146
|
+
data: string;
|
|
147
|
+
driverParam: string;
|
|
148
|
+
notNull: true;
|
|
149
|
+
hasDefault: false;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: [string, ...string[]];
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
159
|
+
name: "created_at";
|
|
160
|
+
tableName: "private_icon_assets";
|
|
161
|
+
dataType: "number";
|
|
162
|
+
columnType: "PgBigInt53";
|
|
163
|
+
data: number;
|
|
164
|
+
driverParam: string | number;
|
|
165
|
+
notNull: true;
|
|
166
|
+
hasDefault: false;
|
|
167
|
+
isPrimaryKey: false;
|
|
168
|
+
isAutoincrement: false;
|
|
169
|
+
hasRuntimeDefault: false;
|
|
170
|
+
enumValues: undefined;
|
|
171
|
+
baseColumn: never;
|
|
172
|
+
identity: undefined;
|
|
173
|
+
generated: undefined;
|
|
174
|
+
}, {}, {}>;
|
|
175
|
+
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
176
|
+
name: "updated_at";
|
|
177
|
+
tableName: "private_icon_assets";
|
|
178
|
+
dataType: "number";
|
|
179
|
+
columnType: "PgBigInt53";
|
|
180
|
+
data: number;
|
|
181
|
+
driverParam: string | number;
|
|
182
|
+
notNull: true;
|
|
183
|
+
hasDefault: false;
|
|
184
|
+
isPrimaryKey: false;
|
|
185
|
+
isAutoincrement: false;
|
|
186
|
+
hasRuntimeDefault: false;
|
|
187
|
+
enumValues: undefined;
|
|
188
|
+
baseColumn: never;
|
|
189
|
+
identity: undefined;
|
|
190
|
+
generated: undefined;
|
|
191
|
+
}, {}, {}>;
|
|
192
|
+
};
|
|
193
|
+
dialect: 'pg';
|
|
194
|
+
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { bigint, index, table, text } from "../db/schema.js";
|
|
2
|
+
export const iconAssets = table("private_icon_assets", {
|
|
3
|
+
id: text("id").primaryKey(),
|
|
4
|
+
ownerEmail: text("owner_email").notNull(),
|
|
5
|
+
orgId: text("org_id"),
|
|
6
|
+
mimeType: text("mime_type").notNull(),
|
|
7
|
+
filename: text("filename"),
|
|
8
|
+
alt: text("alt"),
|
|
9
|
+
size: bigint("size", { mode: "number" }).notNull(),
|
|
10
|
+
sha256: text("sha256").notNull(),
|
|
11
|
+
blobHandleJson: text("blob_handle_json").notNull(),
|
|
12
|
+
createdAt: bigint("created_at", { mode: "number" }).notNull(),
|
|
13
|
+
updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
|
|
14
|
+
}, (table) => [
|
|
15
|
+
index("private_icon_assets_owner_org_created_idx").on(table.ownerEmail, table.orgId, table.createdAt),
|
|
16
|
+
]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const MAX_ICON_ASSET_BYTES: number;
|
|
2
|
+
export interface IconAssetScope {
|
|
3
|
+
ownerEmail: string;
|
|
4
|
+
orgId?: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface IconAsset {
|
|
7
|
+
id: string;
|
|
8
|
+
ownerEmail: string;
|
|
9
|
+
orgId: string | null;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
filename: string | null;
|
|
12
|
+
alt: string | null;
|
|
13
|
+
size: number;
|
|
14
|
+
sha256: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
}
|
|
18
|
+
export interface IconAssetRead {
|
|
19
|
+
asset: IconAsset;
|
|
20
|
+
data: Uint8Array;
|
|
21
|
+
mimeType: string;
|
|
22
|
+
}
|
|
23
|
+
/** Uploads validated bytes and persists only the opaque private blob handle. */
|
|
24
|
+
export declare function putIconAsset(input: IconAssetScope & {
|
|
25
|
+
data: Uint8Array;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
filename?: string;
|
|
28
|
+
alt?: string;
|
|
29
|
+
}): Promise<IconAsset>;
|
|
30
|
+
export declare function getIconAsset(id: string, caller: IconAssetScope): Promise<IconAsset | null>;
|
|
31
|
+
export declare function readIconAsset(id: string, caller: IconAssetScope): Promise<IconAssetRead | null>;
|
|
32
|
+
/** Only call after proving the caller may read a live resource referencing this ID. */
|
|
33
|
+
export declare function readIconAssetForAuthorizedReference(id: string, scope: {
|
|
34
|
+
orgId?: string | null;
|
|
35
|
+
}): Promise<IconAssetRead | null>;
|
|
36
|
+
export declare function listIconAssets(input: IconAssetScope & {
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<IconAsset[]>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { and, desc, eq, isNull } from "drizzle-orm";
|
|
3
|
+
import { createGetDb } from "../db/create-get-db.js";
|
|
4
|
+
import { deletePrivateBlob, putPrivateBlob, readPrivateBlob, } from "../private-blob/index.js";
|
|
5
|
+
import { iconAssets } from "./schema.js";
|
|
6
|
+
import { sanitizeIconSvg } from "./svg.js";
|
|
7
|
+
export const MAX_ICON_ASSET_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
const getDb = createGetDb({ iconAssets });
|
|
9
|
+
function checkedScope(scope) {
|
|
10
|
+
const ownerEmail = scope.ownerEmail?.trim().toLowerCase();
|
|
11
|
+
if (!ownerEmail)
|
|
12
|
+
throw new Error("Icon asset owner is required");
|
|
13
|
+
if (scope.orgId !== undefined &&
|
|
14
|
+
scope.orgId !== null &&
|
|
15
|
+
!scope.orgId.trim()) {
|
|
16
|
+
throw new Error("Icon asset organization is invalid");
|
|
17
|
+
}
|
|
18
|
+
return { ownerEmail, orgId: scope.orgId ?? null };
|
|
19
|
+
}
|
|
20
|
+
function orgCondition(orgId) {
|
|
21
|
+
return orgId ? eq(iconAssets.orgId, orgId) : isNull(iconAssets.orgId);
|
|
22
|
+
}
|
|
23
|
+
function metadata(row) {
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
ownerEmail: row.ownerEmail,
|
|
27
|
+
orgId: row.orgId,
|
|
28
|
+
mimeType: row.mimeType,
|
|
29
|
+
filename: row.filename,
|
|
30
|
+
alt: row.alt,
|
|
31
|
+
size: row.size,
|
|
32
|
+
sha256: row.sha256,
|
|
33
|
+
createdAt: row.createdAt,
|
|
34
|
+
updatedAt: row.updatedAt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function detectMime(data) {
|
|
38
|
+
if (data.length >= 8 &&
|
|
39
|
+
[137, 80, 78, 71, 13, 10, 26, 10].every((byte, index) => data[index] === byte))
|
|
40
|
+
return "image/png";
|
|
41
|
+
if (data.length >= 3 &&
|
|
42
|
+
data[0] === 0xff &&
|
|
43
|
+
data[1] === 0xd8 &&
|
|
44
|
+
data[2] === 0xff)
|
|
45
|
+
return "image/jpeg";
|
|
46
|
+
if (data.length >= 12 &&
|
|
47
|
+
Buffer.from(data.subarray(0, 4)).toString() === "RIFF" &&
|
|
48
|
+
Buffer.from(data.subarray(8, 12)).toString() === "WEBP")
|
|
49
|
+
return "image/webp";
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function validatedBytes(data, mimeType) {
|
|
53
|
+
if (!(data instanceof Uint8Array) ||
|
|
54
|
+
data.byteLength === 0 ||
|
|
55
|
+
data.byteLength > MAX_ICON_ASSET_BYTES) {
|
|
56
|
+
throw new Error("Icon image must be between 1 byte and 5 MiB");
|
|
57
|
+
}
|
|
58
|
+
if (mimeType === "image/svg+xml") {
|
|
59
|
+
const safe = sanitizeIconSvg(data);
|
|
60
|
+
if (safe.byteLength > MAX_ICON_ASSET_BYTES)
|
|
61
|
+
throw new Error("SVG icon exceeds 5 MiB");
|
|
62
|
+
return safe;
|
|
63
|
+
}
|
|
64
|
+
if (!["image/png", "image/jpeg", "image/webp"].includes(mimeType) ||
|
|
65
|
+
detectMime(data) !== mimeType) {
|
|
66
|
+
throw new Error("Icon image type does not match PNG, JPEG, or WebP content");
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
function parseHandle(row) {
|
|
71
|
+
const handle = JSON.parse(row.blobHandleJson);
|
|
72
|
+
if (!handle ||
|
|
73
|
+
typeof handle.id !== "string" ||
|
|
74
|
+
typeof handle.provider !== "string" ||
|
|
75
|
+
handle.opaque !== true) {
|
|
76
|
+
throw new Error("Icon asset has an invalid private blob handle");
|
|
77
|
+
}
|
|
78
|
+
return handle;
|
|
79
|
+
}
|
|
80
|
+
async function readRow(row) {
|
|
81
|
+
const read = await readPrivateBlob(parseHandle(row));
|
|
82
|
+
const hash = createHash("sha256").update(read.data).digest("hex");
|
|
83
|
+
if (hash !== row.sha256 || read.data.byteLength !== row.size) {
|
|
84
|
+
throw new Error("Icon asset private blob failed integrity verification");
|
|
85
|
+
}
|
|
86
|
+
return { asset: metadata(row), data: read.data, mimeType: row.mimeType };
|
|
87
|
+
}
|
|
88
|
+
/** Uploads validated bytes and persists only the opaque private blob handle. */
|
|
89
|
+
export async function putIconAsset(input) {
|
|
90
|
+
const scope = checkedScope(input);
|
|
91
|
+
const data = validatedBytes(input.data, input.mimeType);
|
|
92
|
+
const id = randomUUID();
|
|
93
|
+
const filename = input.filename?.trim().slice(0, 255) || null;
|
|
94
|
+
const alt = input.alt?.trim().slice(0, 500) || null;
|
|
95
|
+
const handle = await putPrivateBlob({
|
|
96
|
+
data,
|
|
97
|
+
filename: `icon-${id}`,
|
|
98
|
+
mimeType: input.mimeType,
|
|
99
|
+
ownerEmail: scope.ownerEmail,
|
|
100
|
+
});
|
|
101
|
+
if (!handle)
|
|
102
|
+
throw new Error("Private icon storage is unavailable; configure a private blob provider");
|
|
103
|
+
try {
|
|
104
|
+
const read = await readPrivateBlob(handle);
|
|
105
|
+
if (!Buffer.from(read.data).equals(Buffer.from(data))) {
|
|
106
|
+
throw new Error("Private icon storage read-back did not match uploaded bytes");
|
|
107
|
+
}
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const row = {
|
|
110
|
+
id,
|
|
111
|
+
ownerEmail: scope.ownerEmail,
|
|
112
|
+
orgId: scope.orgId ?? null,
|
|
113
|
+
mimeType: input.mimeType,
|
|
114
|
+
filename,
|
|
115
|
+
alt,
|
|
116
|
+
size: data.byteLength,
|
|
117
|
+
sha256: createHash("sha256").update(data).digest("hex"),
|
|
118
|
+
blobHandleJson: JSON.stringify(handle),
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
};
|
|
122
|
+
await getDb().insert(iconAssets).values(row);
|
|
123
|
+
return metadata(row);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
const deletion = await deletePrivateBlob(handle);
|
|
128
|
+
if (!deletion.deleted)
|
|
129
|
+
throw new Error(deletion.reason ?? "blob provider refused deletion");
|
|
130
|
+
}
|
|
131
|
+
catch (cleanupError) {
|
|
132
|
+
throw new AggregateError([error, cleanupError], "Private icon upload failed and blob cleanup failed");
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function getIconAsset(id, caller) {
|
|
138
|
+
const scope = checkedScope(caller);
|
|
139
|
+
const [row] = await getDb()
|
|
140
|
+
.select()
|
|
141
|
+
.from(iconAssets)
|
|
142
|
+
.where(and(eq(iconAssets.id, id), eq(iconAssets.ownerEmail, scope.ownerEmail), orgCondition(scope.orgId)))
|
|
143
|
+
.limit(1);
|
|
144
|
+
return row ? metadata(row) : null;
|
|
145
|
+
}
|
|
146
|
+
export async function readIconAsset(id, caller) {
|
|
147
|
+
const scope = checkedScope(caller);
|
|
148
|
+
const [row] = await getDb()
|
|
149
|
+
.select()
|
|
150
|
+
.from(iconAssets)
|
|
151
|
+
.where(and(eq(iconAssets.id, id), eq(iconAssets.ownerEmail, scope.ownerEmail), orgCondition(scope.orgId)))
|
|
152
|
+
.limit(1);
|
|
153
|
+
return row ? readRow(row) : null;
|
|
154
|
+
}
|
|
155
|
+
/** Only call after proving the caller may read a live resource referencing this ID. */
|
|
156
|
+
export async function readIconAssetForAuthorizedReference(id, scope) {
|
|
157
|
+
const [row] = await getDb()
|
|
158
|
+
.select()
|
|
159
|
+
.from(iconAssets)
|
|
160
|
+
.where(and(eq(iconAssets.id, id), orgCondition(scope.orgId)))
|
|
161
|
+
.limit(1);
|
|
162
|
+
return row ? readRow(row) : null;
|
|
163
|
+
}
|
|
164
|
+
export async function listIconAssets(input) {
|
|
165
|
+
const scope = checkedScope(input);
|
|
166
|
+
const limit = input.limit ?? 50;
|
|
167
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
168
|
+
throw new Error("Icon asset limit must be between 1 and 100");
|
|
169
|
+
const rows = await getDb()
|
|
170
|
+
.select({
|
|
171
|
+
id: iconAssets.id,
|
|
172
|
+
ownerEmail: iconAssets.ownerEmail,
|
|
173
|
+
orgId: iconAssets.orgId,
|
|
174
|
+
mimeType: iconAssets.mimeType,
|
|
175
|
+
filename: iconAssets.filename,
|
|
176
|
+
alt: iconAssets.alt,
|
|
177
|
+
size: iconAssets.size,
|
|
178
|
+
sha256: iconAssets.sha256,
|
|
179
|
+
createdAt: iconAssets.createdAt,
|
|
180
|
+
updatedAt: iconAssets.updatedAt,
|
|
181
|
+
})
|
|
182
|
+
.from(iconAssets)
|
|
183
|
+
.where(and(eq(iconAssets.ownerEmail, scope.ownerEmail), orgCondition(scope.orgId)))
|
|
184
|
+
.orderBy(desc(iconAssets.createdAt))
|
|
185
|
+
.limit(limit);
|
|
186
|
+
return rows;
|
|
187
|
+
}
|