@freema/drobek-modules 0.3.3 → 0.5.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/{chunk-WHKPW5MP.js → chunk-B7PEIWK7.js} +133 -82
- package/dist/index.d.ts +34 -4
- package/dist/index.js +63 -1
- package/dist/{mail-guard.d-BKJhsVA2.d.ts → mail-guard.d-BHuy9yy0.d.ts} +39 -0
- package/dist/migrations/core/0028_gallery_duplicate.sql +8 -0
- package/dist/migrations/core/0029_gallery_likes.sql +19 -0
- package/dist/migrations/core/meta/_journal.json +14 -0
- package/dist/testing.js +98 -19
- package/package.json +2 -2
|
@@ -78,6 +78,7 @@ var CORE_ERROR_CODES = [
|
|
|
78
78
|
"user_confirmation_required",
|
|
79
79
|
"gallery_hidden",
|
|
80
80
|
"gallery_disabled",
|
|
81
|
+
"not_duplicable",
|
|
81
82
|
"publish_not_approved",
|
|
82
83
|
"publish_blocked",
|
|
83
84
|
// app assets (MCP asset tools, the upload URL)
|
|
@@ -95,6 +96,8 @@ var CORE_ERROR_CODES = [
|
|
|
95
96
|
"domain_taken",
|
|
96
97
|
"domain_not_verified",
|
|
97
98
|
"dns_unavailable",
|
|
99
|
+
// proxy upstreams (MCP upstream tools)
|
|
100
|
+
"upstream_already_registered",
|
|
98
101
|
// compile.errors[]
|
|
99
102
|
"build_error",
|
|
100
103
|
"unresolved_import",
|
|
@@ -221,6 +224,8 @@ __export(schema_exports, {
|
|
|
221
224
|
blobs: () => blobs,
|
|
222
225
|
compileStatusEnum: () => compileStatusEnum,
|
|
223
226
|
domains: () => domains,
|
|
227
|
+
galleryLikes: () => galleryLikes,
|
|
228
|
+
galleryOpens: () => galleryOpens,
|
|
224
229
|
membershipRoleEnum: () => membershipRoleEnum,
|
|
225
230
|
memberships: () => memberships,
|
|
226
231
|
moduleConfigs: () => moduleConfigs,
|
|
@@ -340,9 +345,16 @@ var apps = pgTable("apps", {
|
|
|
340
345
|
/** NSO-340: the gallery's one-line public description (plain text, ≤ 160 chars). */
|
|
341
346
|
galleryDescription: text("gallery_description"),
|
|
342
347
|
/** NSO-340: a super-admin hid the gallery entry (non-null = hidden, whatever the owner sets). */
|
|
343
|
-
galleryHiddenAt: timestamp("gallery_hidden_at")
|
|
348
|
+
galleryHiddenAt: timestamp("gallery_hidden_at"),
|
|
349
|
+
/** NSO-340: the owner lets signed-in people duplicate the listed app into their own workspace. */
|
|
350
|
+
galleryAllowDuplicate: boolean("gallery_allow_duplicate").notNull().default(false),
|
|
351
|
+
/** NSO-340: the gallery app this one was duplicated from (null when the source is gone or none). */
|
|
352
|
+
duplicatedFromAppId: text("duplicated_from_app_id").references(() => apps.id, { onDelete: "set null" }),
|
|
353
|
+
/** The source's slug at duplication time, kept when the source is deleted. */
|
|
354
|
+
duplicatedFromSlug: text("duplicated_from_slug")
|
|
344
355
|
}, (t) => [
|
|
345
356
|
uniqueIndex("apps_slug_uq").on(t.slug),
|
|
357
|
+
index("apps_duplicated_from_idx").on(t.duplicatedFromAppId).where(sql`${t.duplicatedFromAppId} IS NOT NULL`),
|
|
346
358
|
// The public gallery page reads listed apps newest-published first (NSO-340).
|
|
347
359
|
index("apps_gallery_idx").on(t.publishedAt.desc(), t.slug.desc()).where(sql`${t.galleryListed}`),
|
|
348
360
|
index("apps_workspace_idx").on(t.workspaceId),
|
|
@@ -352,6 +364,16 @@ var apps = pgTable("apps", {
|
|
|
352
364
|
// words). A deleted app may carry its released-slug tombstone instead (0016).
|
|
353
365
|
check("apps_slug_format", sql`(${t.slug} ~ '^[a-z0-9]+(-[a-z0-9]+)*$' AND char_length(${t.slug}) BETWEEN 3 AND 40) OR (${t.deletedAt} IS NOT NULL AND ${t.slug} ~ '^[a-z0-9]+(-[a-z0-9]+)*~deleted-[a-z0-9]+$')`)
|
|
354
366
|
]);
|
|
367
|
+
var galleryLikes = pgTable("gallery_likes", {
|
|
368
|
+
appId: text("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
|
|
369
|
+
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
370
|
+
createdAt: timestamp("created_at").notNull().defaultNow()
|
|
371
|
+
}, (t) => [primaryKey({ columns: [t.appId, t.userId] })]);
|
|
372
|
+
var galleryOpens = pgTable("gallery_opens", {
|
|
373
|
+
appId: text("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
|
|
374
|
+
day: text("day").notNull(),
|
|
375
|
+
count: integer("count").notNull().default(0)
|
|
376
|
+
}, (t) => [primaryKey({ columns: [t.appId, t.day] })]);
|
|
355
377
|
var blobs = pgTable("blobs", {
|
|
356
378
|
sha256: text("sha256").primaryKey(),
|
|
357
379
|
bytes: bytea("bytes").notNull(),
|
|
@@ -695,16 +717,16 @@ async function runJournalMigrations(opts) {
|
|
|
695
717
|
const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_URL;
|
|
696
718
|
if (!databaseUrl)
|
|
697
719
|
throw new Error("DATABASE_URL is required");
|
|
698
|
-
const
|
|
720
|
+
const sql17 = postgres2(databaseUrl, { max: 1, connect_timeout: 10, onnotice: () => {
|
|
699
721
|
} });
|
|
700
722
|
try {
|
|
701
|
-
await migrate(drizzle2(
|
|
723
|
+
await migrate(drizzle2(sql17), {
|
|
702
724
|
migrationsFolder: opts.migrationsFolder,
|
|
703
725
|
migrationsTable: opts.migrationsTable,
|
|
704
726
|
migrationsSchema: "drizzle"
|
|
705
727
|
});
|
|
706
728
|
} finally {
|
|
707
|
-
await
|
|
729
|
+
await sql17.end({ timeout: 5 });
|
|
708
730
|
}
|
|
709
731
|
}
|
|
710
732
|
|
|
@@ -921,12 +943,12 @@ function looksLikeSvg(head) {
|
|
|
921
943
|
}
|
|
922
944
|
if (!doctype && matchesAt(DOCTYPE_SVG_RE, head, i)) {
|
|
923
945
|
doctype = true;
|
|
924
|
-
const
|
|
946
|
+
const gt2 = head.indexOf(">", i);
|
|
925
947
|
const bracket = head.indexOf("[", i);
|
|
926
|
-
if (
|
|
948
|
+
if (gt2 < 0)
|
|
927
949
|
return false;
|
|
928
|
-
if (bracket < 0 ||
|
|
929
|
-
i =
|
|
950
|
+
if (bracket < 0 || gt2 < bracket) {
|
|
951
|
+
i = gt2 + 1;
|
|
930
952
|
continue;
|
|
931
953
|
}
|
|
932
954
|
const close = head.indexOf("]", bracket + 1);
|
|
@@ -952,6 +974,20 @@ function hasControlBytes(chunk) {
|
|
|
952
974
|
return false;
|
|
953
975
|
}
|
|
954
976
|
|
|
977
|
+
// packages/core/dist/cookies.js
|
|
978
|
+
function readCookieValue(header, name) {
|
|
979
|
+
if (!header)
|
|
980
|
+
return null;
|
|
981
|
+
for (const part of header.split(";")) {
|
|
982
|
+
const eq22 = part.indexOf("=");
|
|
983
|
+
if (eq22 === -1)
|
|
984
|
+
continue;
|
|
985
|
+
if (part.slice(0, eq22).trim() === name)
|
|
986
|
+
return part.slice(eq22 + 1).trim();
|
|
987
|
+
}
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
955
991
|
// packages/core/dist/redis.js
|
|
956
992
|
import { Redis } from "ioredis";
|
|
957
993
|
var client = null;
|
|
@@ -1182,6 +1218,10 @@ var AUDIT_ACTIONS = {
|
|
|
1182
1218
|
appGalleryHidden: "app.gallery_hidden",
|
|
1183
1219
|
/** NSO-340: a super-admin showed a hidden gallery entry again. */
|
|
1184
1220
|
appGalleryUnhidden: "app.gallery_unhidden",
|
|
1221
|
+
/** NSO-340: this app was created as a copy of a gallery app (meta: from = the source slug, version). */
|
|
1222
|
+
appDuplicate: "app.duplicate",
|
|
1223
|
+
/** NSO-340: someone duplicated this gallery app into their own workspace (no details about them). */
|
|
1224
|
+
appDuplicated: "app.duplicated",
|
|
1185
1225
|
/** NSO-346: a super-admin enabled an opt-in platform module for the workspace (meta: module). */
|
|
1186
1226
|
moduleWorkspaceEnable: "module.workspace_enable",
|
|
1187
1227
|
/** NSO-346: a super-admin disabled an opt-in platform module for the workspace (meta: module). */
|
|
@@ -1228,7 +1268,7 @@ async function writeAudit(entry, executor) {
|
|
|
1228
1268
|
}
|
|
1229
1269
|
|
|
1230
1270
|
// packages/audit/dist/read.server.js
|
|
1231
|
-
import { and, desc, eq, lt, or } from "drizzle-orm";
|
|
1271
|
+
import { and, desc, eq, gte, lt, or } from "drizzle-orm";
|
|
1232
1272
|
|
|
1233
1273
|
// packages/audit/dist/retention.server.js
|
|
1234
1274
|
import { lt as lt2 } from "drizzle-orm";
|
|
@@ -1253,7 +1293,7 @@ var DEFAULT_APPS_MAX_PER_WORKSPACE = 50;
|
|
|
1253
1293
|
|
|
1254
1294
|
// packages/apps/dist/versions.server.js
|
|
1255
1295
|
import { createHash as createHash3 } from "node:crypto";
|
|
1256
|
-
import { and as and8, desc as desc5, eq as eq8, gte, inArray as inArray4, sql as sql5 } from "drizzle-orm";
|
|
1296
|
+
import { and as and8, desc as desc5, eq as eq8, gte as gte2, inArray as inArray4, sql as sql5 } from "drizzle-orm";
|
|
1257
1297
|
|
|
1258
1298
|
// packages/compile/dist/compiler.js
|
|
1259
1299
|
import { mkdtempSync } from "node:fs";
|
|
@@ -1925,7 +1965,8 @@ var reportedAppColumns = {
|
|
|
1925
1965
|
name: apps.name,
|
|
1926
1966
|
workspaceId: apps.workspaceId,
|
|
1927
1967
|
workspaceSlug: workspaces.slug,
|
|
1928
|
-
lockedReason: apps.lockedReason
|
|
1968
|
+
lockedReason: apps.lockedReason,
|
|
1969
|
+
published: sql2`(${apps.publishedVersionId} IS NOT NULL)`.mapWith(Boolean)
|
|
1929
1970
|
};
|
|
1930
1971
|
var SCAN_MAX_FILE = 1024 * 1024;
|
|
1931
1972
|
var SCAN_MAX_TOTAL = 4 * 1024 * 1024;
|
|
@@ -2373,7 +2414,27 @@ var CONTROL_RE = new RegExp("[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]+", "
|
|
|
2373
2414
|
|
|
2374
2415
|
// packages/apps/dist/gallery.server.js
|
|
2375
2416
|
import { and as and12, asc as asc3, desc as desc6, eq as eq12, isNotNull as isNotNull7, isNull as isNull5, sql as sql8 } from "drizzle-orm";
|
|
2376
|
-
var
|
|
2417
|
+
var baseColumns = {
|
|
2418
|
+
slug: apps.slug,
|
|
2419
|
+
name: apps.name,
|
|
2420
|
+
description: apps.galleryDescription,
|
|
2421
|
+
publishedAt: apps.publishedAt,
|
|
2422
|
+
// Names only, from the current saved configuration. A correlated aggregate
|
|
2423
|
+
// keeps pagination/counts intact and avoids a query per card. No extension
|
|
2424
|
+
// or module runtime dependency is needed by this public core API.
|
|
2425
|
+
modules: sql8`(select coalesce(jsonb_agg(${moduleConfigs.module} order by ${moduleConfigs.module}), '[]'::jsonb)
|
|
2426
|
+
from ${moduleConfigs} where ${moduleConfigs.appId} = ${apps.id}
|
|
2427
|
+
and jsonb_typeof(${moduleConfigs.config}) = 'object' and ${moduleConfigs.config} <> '{}'::jsonb)`,
|
|
2428
|
+
duplicable: apps.galleryAllowDuplicate,
|
|
2429
|
+
// Live copies; the alias keeps the outer "apps" row in reach of the subquery.
|
|
2430
|
+
duplicates: sql8`(select count(*)::int from "apps" as "copies" where "copies"."duplicated_from_app_id" = "apps"."id" and "copies"."deleted_at" is null)`
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
// packages/apps/dist/duplicate.server.js
|
|
2434
|
+
import { and as and13, count as count2, eq as eq13, gt, isNull as isNull6, sql as sql9 } from "drizzle-orm";
|
|
2435
|
+
|
|
2436
|
+
// packages/apps/dist/gallery-engagement.server.js
|
|
2437
|
+
import { and as and14, eq as eq14, gte as gte3, sql as sql10 } from "drizzle-orm";
|
|
2377
2438
|
|
|
2378
2439
|
// packages/modules/dist/limits.js
|
|
2379
2440
|
var LIMITS_CACHE_TTL_SEC = 60;
|
|
@@ -2570,20 +2631,8 @@ function endUserEpochKey(appId) {
|
|
|
2570
2631
|
}
|
|
2571
2632
|
var SESSION_PROVIDER_RE = /^[a-z][a-z0-9]{1,15}$/;
|
|
2572
2633
|
var SESSION_CONNECTION_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
2573
|
-
function readCookie(header, name) {
|
|
2574
|
-
if (!header)
|
|
2575
|
-
return null;
|
|
2576
|
-
for (const part of header.split(";")) {
|
|
2577
|
-
const eq20 = part.indexOf("=");
|
|
2578
|
-
if (eq20 === -1)
|
|
2579
|
-
continue;
|
|
2580
|
-
if (part.slice(0, eq20).trim() === name)
|
|
2581
|
-
return part.slice(eq20 + 1).trim();
|
|
2582
|
-
}
|
|
2583
|
-
return null;
|
|
2584
|
-
}
|
|
2585
2634
|
function readEndUserToken(cookieHeader, secure) {
|
|
2586
|
-
const token =
|
|
2635
|
+
const token = readCookieValue(cookieHeader, endUserCookieName(secure));
|
|
2587
2636
|
return token && END_USER_TOKEN_RE.test(token) ? token : null;
|
|
2588
2637
|
}
|
|
2589
2638
|
function parseEndUserSession(raw) {
|
|
@@ -2684,7 +2733,7 @@ function cookiePrincipalResolver(opts) {
|
|
|
2684
2733
|
}
|
|
2685
2734
|
|
|
2686
2735
|
// packages/modules/dist/secrets.server.js
|
|
2687
|
-
import { and as
|
|
2736
|
+
import { and as and16, eq as eq16, inArray as inArray7 } from "drizzle-orm";
|
|
2688
2737
|
|
|
2689
2738
|
// packages/proxy/dist/errors.js
|
|
2690
2739
|
var ProxyError = class extends Error {
|
|
@@ -2810,7 +2859,7 @@ import https from "node:https";
|
|
|
2810
2859
|
var DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2811
2860
|
|
|
2812
2861
|
// packages/proxy/dist/upstreams.server.js
|
|
2813
|
-
import { and as
|
|
2862
|
+
import { and as and15, eq as eq15, inArray as inArray6, sql as sql11 } from "drizzle-orm";
|
|
2814
2863
|
|
|
2815
2864
|
// packages/proxy/dist/forward.server.js
|
|
2816
2865
|
import { promisify } from "node:util";
|
|
@@ -2884,11 +2933,11 @@ async function setModuleSecret(input) {
|
|
|
2884
2933
|
});
|
|
2885
2934
|
}
|
|
2886
2935
|
async function deleteModuleSecret(appId, module, name) {
|
|
2887
|
-
const rows = await getDb().delete(moduleSecrets).where(
|
|
2936
|
+
const rows = await getDb().delete(moduleSecrets).where(and16(eq16(moduleSecrets.appId, appId), eq16(moduleSecrets.module, module), eq16(moduleSecrets.name, name))).returning({ name: moduleSecrets.name });
|
|
2888
2937
|
return rows.length > 0;
|
|
2889
2938
|
}
|
|
2890
2939
|
async function getModuleSecret(appId, module, name, env) {
|
|
2891
|
-
const [row] = await getDb().select().from(moduleSecrets).where(
|
|
2940
|
+
const [row] = await getDb().select().from(moduleSecrets).where(and16(eq16(moduleSecrets.appId, appId), eq16(moduleSecrets.module, module), eq16(moduleSecrets.name, name))).limit(1);
|
|
2892
2941
|
if (!row)
|
|
2893
2942
|
return null;
|
|
2894
2943
|
return decryptSecret({ ciphertext: row.ciphertext, iv: row.iv, authTag: row.authTag, wrappedDek: row.wrappedDek, kekId: row.kekId }, env);
|
|
@@ -2896,18 +2945,18 @@ async function getModuleSecret(appId, module, name, env) {
|
|
|
2896
2945
|
async function secretsStatus(appId, module, names) {
|
|
2897
2946
|
if (names.length === 0)
|
|
2898
2947
|
return /* @__PURE__ */ new Map();
|
|
2899
|
-
const rows = await getDb().select({ name: moduleSecrets.name, updatedAt: moduleSecrets.updatedAt }).from(moduleSecrets).where(
|
|
2948
|
+
const rows = await getDb().select({ name: moduleSecrets.name, updatedAt: moduleSecrets.updatedAt }).from(moduleSecrets).where(and16(eq16(moduleSecrets.appId, appId), eq16(moduleSecrets.module, module), inArray7(moduleSecrets.name, names)));
|
|
2900
2949
|
return new Map(rows.map((r) => [r.name, r.updatedAt]));
|
|
2901
2950
|
}
|
|
2902
2951
|
async function secretsSet(appId, module, names) {
|
|
2903
2952
|
if (names.length === 0)
|
|
2904
2953
|
return /* @__PURE__ */ new Set();
|
|
2905
|
-
const rows = await getDb().select({ name: moduleSecrets.name }).from(moduleSecrets).where(
|
|
2954
|
+
const rows = await getDb().select({ name: moduleSecrets.name }).from(moduleSecrets).where(and16(eq16(moduleSecrets.appId, appId), eq16(moduleSecrets.module, module), inArray7(moduleSecrets.name, names)));
|
|
2906
2955
|
return new Set(rows.map((r) => r.name));
|
|
2907
2956
|
}
|
|
2908
2957
|
|
|
2909
2958
|
// packages/modules/dist/configs.server.js
|
|
2910
|
-
import { and as
|
|
2959
|
+
import { and as and17, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
2911
2960
|
function asObject2(v) {
|
|
2912
2961
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
2913
2962
|
}
|
|
@@ -2924,7 +2973,7 @@ function asPending(v) {
|
|
|
2924
2973
|
};
|
|
2925
2974
|
}
|
|
2926
2975
|
async function readConfigRow(appId, module, executor = getDb()) {
|
|
2927
|
-
const [row] = await executor.select().from(moduleConfigs).where(
|
|
2976
|
+
const [row] = await executor.select().from(moduleConfigs).where(and17(eq17(moduleConfigs.appId, appId), eq17(moduleConfigs.module, module))).limit(1);
|
|
2928
2977
|
if (!row)
|
|
2929
2978
|
return { config: {}, pending: null, updatedAt: null };
|
|
2930
2979
|
return { config: asObject2(row.config), pending: asPending(row.pending), updatedAt: row.updatedAt };
|
|
@@ -2933,7 +2982,7 @@ async function readConfigRows(appId, modules) {
|
|
|
2933
2982
|
const out = /* @__PURE__ */ new Map();
|
|
2934
2983
|
if (modules.length === 0)
|
|
2935
2984
|
return out;
|
|
2936
|
-
const rows = await getDb().select().from(moduleConfigs).where(
|
|
2985
|
+
const rows = await getDb().select().from(moduleConfigs).where(and17(eq17(moduleConfigs.appId, appId), inArray8(moduleConfigs.module, modules)));
|
|
2937
2986
|
for (const r of rows) {
|
|
2938
2987
|
out.set(r.module, { config: asObject2(r.config), pending: asPending(r.pending), updatedAt: r.updatedAt });
|
|
2939
2988
|
}
|
|
@@ -2942,7 +2991,7 @@ async function readConfigRows(appId, modules) {
|
|
|
2942
2991
|
async function withLockedConfig(appId, module, fn) {
|
|
2943
2992
|
return getDb().transaction(async (tx) => {
|
|
2944
2993
|
await tx.insert(moduleConfigs).values({ appId, module }).onConflictDoNothing();
|
|
2945
|
-
const [row] = await tx.select().from(moduleConfigs).where(
|
|
2994
|
+
const [row] = await tx.select().from(moduleConfigs).where(and17(eq17(moduleConfigs.appId, appId), eq17(moduleConfigs.module, module))).for("update");
|
|
2946
2995
|
const current = {
|
|
2947
2996
|
config: asObject2(row?.config),
|
|
2948
2997
|
pending: asPending(row?.pending),
|
|
@@ -2954,7 +3003,7 @@ async function withLockedConfig(appId, module, fn) {
|
|
|
2954
3003
|
set.config = next.config;
|
|
2955
3004
|
if (next.pending !== void 0)
|
|
2956
3005
|
set.pending = next.pending;
|
|
2957
|
-
await tx.update(moduleConfigs).set(set).where(
|
|
3006
|
+
await tx.update(moduleConfigs).set(set).where(and17(eq17(moduleConfigs.appId, appId), eq17(moduleConfigs.module, module)));
|
|
2958
3007
|
};
|
|
2959
3008
|
return fn(current, write, tx);
|
|
2960
3009
|
});
|
|
@@ -3415,7 +3464,7 @@ function redisMailGuard(opts) {
|
|
|
3415
3464
|
const ttl = await r.pttl(key);
|
|
3416
3465
|
return ttl > 0 ? ttl / 1e3 : fallbackSec;
|
|
3417
3466
|
};
|
|
3418
|
-
const
|
|
3467
|
+
const count3 = async (r, key, n) => {
|
|
3419
3468
|
const c = await r.incrby(key, n);
|
|
3420
3469
|
if (c === n || await r.pttl(key) < 0)
|
|
3421
3470
|
await r.pexpire(key, HOUR_MS2);
|
|
@@ -3478,7 +3527,7 @@ function redisMailGuard(opts) {
|
|
|
3478
3527
|
if (!refusal) {
|
|
3479
3528
|
const key = mailAppCounterKey(meta.app_id, cls);
|
|
3480
3529
|
const share = appShareOf(budgets, cls);
|
|
3481
|
-
const used = await
|
|
3530
|
+
const used = await count3(r, key, recipients);
|
|
3482
3531
|
if (used > share) {
|
|
3483
3532
|
if (used - recipients <= share) {
|
|
3484
3533
|
log.warn(`module e-mail: an app used its hourly share of ${cls === "sign_in" ? "sign-in codes" : "notifications"}`, {
|
|
@@ -3494,7 +3543,7 @@ function redisMailGuard(opts) {
|
|
|
3494
3543
|
if (!refusal) {
|
|
3495
3544
|
const key = mailWorkspaceCounterKey(meta.workspace_id, cls);
|
|
3496
3545
|
const share = workspaceShareOf(budgets, cls);
|
|
3497
|
-
const used = await
|
|
3546
|
+
const used = await count3(r, key, recipients);
|
|
3498
3547
|
if (used > share) {
|
|
3499
3548
|
if (used - recipients <= share) {
|
|
3500
3549
|
log.warn(`module e-mail: a workspace used its hourly share of ${cls === "sign_in" ? "sign-in codes" : "notifications"}`, {
|
|
@@ -3508,7 +3557,7 @@ function redisMailGuard(opts) {
|
|
|
3508
3557
|
}
|
|
3509
3558
|
}
|
|
3510
3559
|
if (!refusal) {
|
|
3511
|
-
const used = await
|
|
3560
|
+
const used = await count3(r, MAIL_COUNTER_KEYS[cls], recipients);
|
|
3512
3561
|
if (used > budgets[cls]) {
|
|
3513
3562
|
await r.set(MAIL_PAUSE_KEYS[cls], "1", "PX", pauseMs);
|
|
3514
3563
|
await r.set(MAIL_COUNTER_KEYS[cls], "0", "PX", pauseMs);
|
|
@@ -4117,43 +4166,43 @@ function readModulesLock(modulesDir) {
|
|
|
4117
4166
|
// packages/modules/dist/migration-lint.js
|
|
4118
4167
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4119
4168
|
import { join as join7 } from "node:path";
|
|
4120
|
-
function blank(
|
|
4121
|
-
const out =
|
|
4169
|
+
function blank(sql17) {
|
|
4170
|
+
const out = sql17.split("");
|
|
4122
4171
|
const wipe = (from, to) => {
|
|
4123
4172
|
for (let k = from; k < to; k++)
|
|
4124
4173
|
if (out[k] !== "\n")
|
|
4125
4174
|
out[k] = " ";
|
|
4126
4175
|
};
|
|
4127
4176
|
let i = 0;
|
|
4128
|
-
while (i <
|
|
4129
|
-
const c =
|
|
4130
|
-
if (c === "-" &&
|
|
4131
|
-
const end =
|
|
4132
|
-
const stop = end === -1 ?
|
|
4177
|
+
while (i < sql17.length) {
|
|
4178
|
+
const c = sql17[i];
|
|
4179
|
+
if (c === "-" && sql17[i + 1] === "-") {
|
|
4180
|
+
const end = sql17.indexOf("\n", i);
|
|
4181
|
+
const stop = end === -1 ? sql17.length : end;
|
|
4133
4182
|
wipe(i, stop);
|
|
4134
4183
|
i = stop;
|
|
4135
|
-
} else if (c === "/" &&
|
|
4136
|
-
const end =
|
|
4137
|
-
const stop = end === -1 ?
|
|
4184
|
+
} else if (c === "/" && sql17[i + 1] === "*") {
|
|
4185
|
+
const end = sql17.indexOf("*/", i + 2);
|
|
4186
|
+
const stop = end === -1 ? sql17.length : end + 2;
|
|
4138
4187
|
wipe(i, stop);
|
|
4139
4188
|
i = stop;
|
|
4140
4189
|
} else if (c === "'") {
|
|
4141
4190
|
let j = i + 1;
|
|
4142
|
-
while (j <
|
|
4143
|
-
if (
|
|
4191
|
+
while (j < sql17.length) {
|
|
4192
|
+
if (sql17[j] === "'" && sql17[j + 1] === "'")
|
|
4144
4193
|
j += 2;
|
|
4145
|
-
else if (
|
|
4194
|
+
else if (sql17[j] === "'")
|
|
4146
4195
|
break;
|
|
4147
4196
|
else
|
|
4148
4197
|
j++;
|
|
4149
4198
|
}
|
|
4150
|
-
wipe(i, Math.min(j + 1,
|
|
4199
|
+
wipe(i, Math.min(j + 1, sql17.length));
|
|
4151
4200
|
i = j + 1;
|
|
4152
4201
|
} else if (c === '"') {
|
|
4153
|
-
const end =
|
|
4154
|
-
i = end === -1 ?
|
|
4202
|
+
const end = sql17.indexOf('"', i + 1);
|
|
4203
|
+
i = end === -1 ? sql17.length : end + 1;
|
|
4155
4204
|
} else if (c === "$") {
|
|
4156
|
-
const tag = /^\$[A-Za-z_]*\$/.exec(
|
|
4205
|
+
const tag = /^\$[A-Za-z_]*\$/.exec(sql17.slice(i));
|
|
4157
4206
|
if (tag) {
|
|
4158
4207
|
wipe(i, i + tag[0].length);
|
|
4159
4208
|
i += tag[0].length;
|
|
@@ -4173,8 +4222,8 @@ function splitName(raw) {
|
|
|
4173
4222
|
const unq = (p) => p.startsWith('"') ? p.slice(1, -1).replace(/""/g, '"') : p.toLowerCase();
|
|
4174
4223
|
return parts.length === 2 ? { schema: unq(parts[0]), name: unq(parts[1]) } : { schema: null, name: unq(parts[0] ?? "") };
|
|
4175
4224
|
}
|
|
4176
|
-
function lintMigrationSql(name,
|
|
4177
|
-
const text2 = blank(
|
|
4225
|
+
function lintMigrationSql(name, sql17, file = "migration.sql") {
|
|
4226
|
+
const text2 = blank(sql17);
|
|
4178
4227
|
const prefix = `mod_${name}`;
|
|
4179
4228
|
const own = (raw) => {
|
|
4180
4229
|
const { schema, name: n } = splitName(raw);
|
|
@@ -4993,20 +5042,20 @@ var LOGS_RETENTION_DAYS = 30;
|
|
|
4993
5042
|
import { createHash as createHash8 } from "node:crypto";
|
|
4994
5043
|
|
|
4995
5044
|
// packages/insights/dist/resolve.server.js
|
|
4996
|
-
import { and as
|
|
5045
|
+
import { and as and18, eq as eq18, isNull as isNull7 } from "drizzle-orm";
|
|
4997
5046
|
|
|
4998
5047
|
// packages/insights/dist/signals.server.js
|
|
4999
|
-
import { sql as
|
|
5048
|
+
import { sql as sql12 } from "drizzle-orm";
|
|
5000
5049
|
var SIGNAL_TTL_SEC = (LOGS_RETENTION_DAYS + 1) * 24 * 60 * 60;
|
|
5001
|
-
function
|
|
5050
|
+
function utcDay2(d = /* @__PURE__ */ new Date()) {
|
|
5002
5051
|
return d.toISOString().slice(0, 10);
|
|
5003
5052
|
}
|
|
5004
5053
|
|
|
5005
5054
|
// packages/insights/dist/beacon.server.js
|
|
5006
|
-
import { sql as
|
|
5055
|
+
import { sql as sql13 } from "drizzle-orm";
|
|
5007
5056
|
|
|
5008
5057
|
// packages/insights/dist/query.server.js
|
|
5009
|
-
import { and as
|
|
5058
|
+
import { and as and19, desc as desc7, eq as eq19, gte as gte4 } from "drizzle-orm";
|
|
5010
5059
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5011
5060
|
|
|
5012
5061
|
// packages/insights/dist/logs.js
|
|
@@ -5022,10 +5071,10 @@ function statusClass(status) {
|
|
|
5022
5071
|
}
|
|
5023
5072
|
|
|
5024
5073
|
// packages/insights/dist/logs.server.js
|
|
5025
|
-
import { and as
|
|
5074
|
+
import { and as and20, desc as desc8, eq as eq20, gte as gte5, lt as lt5, sql as sql15 } from "drizzle-orm";
|
|
5026
5075
|
|
|
5027
5076
|
// packages/insights/dist/module-stats.server.js
|
|
5028
|
-
import { sql as
|
|
5077
|
+
import { sql as sql14 } from "drizzle-orm";
|
|
5029
5078
|
var MODULE_STATS_FLUSH_SEC = 60;
|
|
5030
5079
|
var MODULE_STATS_TTL_SEC = (LOGS_RETENTION_DAYS + 1) * 24 * 60 * 60;
|
|
5031
5080
|
var sharedRedis = () => getRedis();
|
|
@@ -5038,7 +5087,7 @@ function flushMarkerKey(appId, day) {
|
|
|
5038
5087
|
async function recordModuleRequest(appId, module, status, opts = {}) {
|
|
5039
5088
|
try {
|
|
5040
5089
|
const r = (opts.redis ?? sharedRedis)();
|
|
5041
|
-
const day =
|
|
5090
|
+
const day = utcDay2(opts.now ?? /* @__PURE__ */ new Date());
|
|
5042
5091
|
const key = moduleCountersKey(appId, day);
|
|
5043
5092
|
if (await r.hincrby(key, `${module}:${statusClass(status)}`, 1) === 1)
|
|
5044
5093
|
await r.expire(key, MODULE_STATS_TTL_SEC);
|
|
@@ -5055,10 +5104,10 @@ function moduleStatRows(appId, day, counters) {
|
|
|
5055
5104
|
const at = field.lastIndexOf(":");
|
|
5056
5105
|
const module = field.slice(0, at);
|
|
5057
5106
|
const cls = field.slice(at + 1);
|
|
5058
|
-
const
|
|
5059
|
-
if (at <= 0 || !STATUS_CLASSES.includes(cls) || !Number.isInteger(
|
|
5107
|
+
const count3 = Number(raw);
|
|
5108
|
+
if (at <= 0 || !STATUS_CLASSES.includes(cls) || !Number.isInteger(count3) || count3 <= 0)
|
|
5060
5109
|
continue;
|
|
5061
|
-
rows.push({ appId, module, statusClass: cls, day, count:
|
|
5110
|
+
rows.push({ appId, module, statusClass: cls, day, count: count3 });
|
|
5062
5111
|
}
|
|
5063
5112
|
return rows;
|
|
5064
5113
|
}
|
|
@@ -5067,7 +5116,7 @@ async function upsertModuleStats(rows) {
|
|
|
5067
5116
|
return;
|
|
5068
5117
|
await getDb().insert(moduleRequestStats).values(rows).onConflictDoUpdate({
|
|
5069
5118
|
target: [moduleRequestStats.appId, moduleRequestStats.module, moduleRequestStats.statusClass, moduleRequestStats.day],
|
|
5070
|
-
set: { count:
|
|
5119
|
+
set: { count: sql14`greatest(${moduleRequestStats.count}, excluded.count)` }
|
|
5071
5120
|
});
|
|
5072
5121
|
}
|
|
5073
5122
|
async function flushModuleRequests(appId, day, opts = {}) {
|
|
@@ -5082,12 +5131,12 @@ async function flushModuleRequests(appId, day, opts = {}) {
|
|
|
5082
5131
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
5083
5132
|
|
|
5084
5133
|
// packages/insights/dist/prune.server.js
|
|
5085
|
-
import { lt as lt6, sql as
|
|
5134
|
+
import { lt as lt6, sql as sql16 } from "drizzle-orm";
|
|
5086
5135
|
var DEFAULT_LOGS_PRUNE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
5087
5136
|
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
5088
5137
|
|
|
5089
5138
|
// packages/modules/dist/runtime.js
|
|
5090
|
-
import { and as
|
|
5139
|
+
import { and as and21, eq as eq21, inArray as inArray9 } from "drizzle-orm";
|
|
5091
5140
|
import { z } from "zod";
|
|
5092
5141
|
function redisRateLimiter(redis) {
|
|
5093
5142
|
return async (key, max, windowMs) => {
|
|
@@ -5134,7 +5183,7 @@ function smtpEmailTransport(log, env = process.env) {
|
|
|
5134
5183
|
};
|
|
5135
5184
|
}
|
|
5136
5185
|
async function appOwnerEmails(db2, workspaceId) {
|
|
5137
|
-
const rows = await db2.select({ email: users.email }).from(memberships).innerJoin(users,
|
|
5186
|
+
const rows = await db2.select({ email: users.email }).from(memberships).innerJoin(users, eq21(users.id, memberships.userId)).where(and21(eq21(memberships.workspaceId, workspaceId), inArray9(memberships.role, ["editor", "workspace-admin"])));
|
|
5138
5187
|
return rows.map((r) => r.email);
|
|
5139
5188
|
}
|
|
5140
5189
|
function uniqueKeyOf(value, unique) {
|
|
@@ -5291,7 +5340,7 @@ var ModuleRuntime = class {
|
|
|
5291
5340
|
return out;
|
|
5292
5341
|
const plan = await this.deps.limits.fromPlan?.(workspaceId) ?? null;
|
|
5293
5342
|
const env = this.deps.limits.defaults();
|
|
5294
|
-
const rows = workspaceId ? await this.deps.db().select({ module: workspaceModules.module, enabledBy: workspaceModules.enabledBy, enabledAt: workspaceModules.enabledAt }).from(workspaceModules).where(
|
|
5343
|
+
const rows = workspaceId ? await this.deps.db().select({ module: workspaceModules.module, enabledBy: workspaceModules.enabledBy, enabledAt: workspaceModules.enabledAt }).from(workspaceModules).where(and21(eq21(workspaceModules.workspaceId, workspaceId), inArray9(workspaceModules.module, modules.map((m) => m.name)))) : [];
|
|
5295
5344
|
const byModule = new Map(rows.map((r) => [r.module, { enabledBy: r.enabledBy, enabledAt: r.enabledAt }]));
|
|
5296
5345
|
for (const m of modules) {
|
|
5297
5346
|
const key = moduleEnabledLimitName(m.name);
|
|
@@ -5371,7 +5420,7 @@ var ModuleRuntime = class {
|
|
|
5371
5420
|
});
|
|
5372
5421
|
}
|
|
5373
5422
|
return this.deps.db().transaction(async (tx) => {
|
|
5374
|
-
const changed = input.enabled ? (await tx.insert(workspaceModules).values({ workspaceId: input.workspaceId, module: m.name, enabledBy: input.actorUserId }).onConflictDoNothing().returning({ module: workspaceModules.module })).length > 0 : (await tx.delete(workspaceModules).where(
|
|
5423
|
+
const changed = input.enabled ? (await tx.insert(workspaceModules).values({ workspaceId: input.workspaceId, module: m.name, enabledBy: input.actorUserId }).onConflictDoNothing().returning({ module: workspaceModules.module })).length > 0 : (await tx.delete(workspaceModules).where(and21(eq21(workspaceModules.workspaceId, input.workspaceId), eq21(workspaceModules.module, m.name))).returning({ module: workspaceModules.module })).length > 0;
|
|
5375
5424
|
if (changed) {
|
|
5376
5425
|
await writeAudit({
|
|
5377
5426
|
workspaceId: input.workspaceId,
|
|
@@ -5444,7 +5493,7 @@ var ModuleRuntime = class {
|
|
|
5444
5493
|
return null;
|
|
5445
5494
|
const deps = this.deps;
|
|
5446
5495
|
const db2 = deps.db();
|
|
5447
|
-
const [row] = await db2.select({ id: apps.id, slug: apps.slug, workspaceId: apps.workspaceId, deletedAt: apps.deletedAt, lockedReason: apps.lockedReason }).from(apps).where(
|
|
5496
|
+
const [row] = await db2.select({ id: apps.id, slug: apps.slug, workspaceId: apps.workspaceId, deletedAt: apps.deletedAt, lockedReason: apps.lockedReason }).from(apps).where(eq21(apps.id, appId)).limit(1);
|
|
5448
5497
|
if (!row || row.deletedAt || row.lockedReason)
|
|
5449
5498
|
return null;
|
|
5450
5499
|
const app = { id: row.id, slug: row.slug, workspaceId: row.workspaceId };
|
|
@@ -5916,7 +5965,7 @@ var ModuleRuntime = class {
|
|
|
5916
5965
|
async workspaceOf(appId) {
|
|
5917
5966
|
if (this.optInModules().length === 0)
|
|
5918
5967
|
return "";
|
|
5919
|
-
const [row] = await this.deps.db().select({ workspaceId: apps.workspaceId }).from(apps).where(
|
|
5968
|
+
const [row] = await this.deps.db().select({ workspaceId: apps.workspaceId }).from(apps).where(eq21(apps.id, appId)).limit(1);
|
|
5920
5969
|
return row?.workspaceId ?? "";
|
|
5921
5970
|
}
|
|
5922
5971
|
requireModule(name) {
|
|
@@ -6047,7 +6096,7 @@ var ModuleRuntime = class {
|
|
|
6047
6096
|
const waiting = await this.pendingSummary(app.id);
|
|
6048
6097
|
if (waiting.length === 0)
|
|
6049
6098
|
return;
|
|
6050
|
-
const [row] = await deps.db().select({ name: apps.name }).from(apps).where(
|
|
6099
|
+
const [row] = await deps.db().select({ name: apps.name }).from(apps).where(eq21(apps.id, app.id)).limit(1);
|
|
6051
6100
|
const mail = pendingMail({
|
|
6052
6101
|
appName: row?.name ?? app.slug,
|
|
6053
6102
|
modules: waiting.map((w) => ({ ...w, confirmUrl: confirmUrl(deps.env, app.workspaceSlug, app.slug, w.module) }))
|
|
@@ -6241,11 +6290,11 @@ var ModuleRuntime = class {
|
|
|
6241
6290
|
}
|
|
6242
6291
|
/** get_logs `requests`: one response of a matched route of an active module (fire-and-forget). */
|
|
6243
6292
|
countRequest(appId, module, status) {
|
|
6244
|
-
const
|
|
6245
|
-
if (!
|
|
6293
|
+
const count3 = this.deps.requestStats;
|
|
6294
|
+
if (!count3)
|
|
6246
6295
|
return;
|
|
6247
6296
|
try {
|
|
6248
|
-
void Promise.resolve(
|
|
6297
|
+
void Promise.resolve(count3(appId, module, status)).catch(() => void 0);
|
|
6249
6298
|
} catch {
|
|
6250
6299
|
}
|
|
6251
6300
|
}
|
|
@@ -6489,6 +6538,8 @@ export {
|
|
|
6489
6538
|
decideAccess,
|
|
6490
6539
|
workspaces,
|
|
6491
6540
|
apps,
|
|
6541
|
+
moduleConfigs,
|
|
6542
|
+
getDb,
|
|
6492
6543
|
noopLogger,
|
|
6493
6544
|
perIpLimitKey,
|
|
6494
6545
|
sniffSignature,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ZodType, z } from 'zod';
|
|
2
2
|
export { z } from 'zod';
|
|
3
|
-
import { L as Logger, M as ModuleSecretDoc, H as HookApp, E as EndUser, D as DB, R as Rule, P as Principal, A as AccessDecision, a as ModuleLimit, b as Limits, C as ConfirmRole, c as AnyModule, d as EmailMessage, e as EmailKind, f as EmailRecipient, U as UploadedFile, g as EndUserListQuery, h as EndUserPage, i as EndUserRecord, O as OwnerFilesPage, j as OwnerFile, k as RecordsCollection, l as RecordsQuery, m as RecordsPage, S as SubmissionsQuery, n as SubmissionsPage, o as MailEnvelope, p as RateLimitResult, q as MailGuard, r as ModuleAvailability, s as ModuleDashboardEditor, t as ModuleErrorDoc, u as SdkBundle, v as ModuleServices, w as EndUserCallbackInput, x as EndUserCallbackResult } from './mail-guard.d-
|
|
4
|
-
export { B as BEACON_SCRIPT_PATH, y as BeaconScript, z as ComposedModuleParts, F as ConfirmContext, G as ConfirmItem, I as ConfirmedContext, J as DrobekModule, K as EndUserAuthority, N as EndUserCallbackApp, Q as FilesAuthority, T as MAIL_COUNTER_KEYS, V as MAIL_PAUSE_KEYS, W as MODULE_CONTRACT_VERSION, X as MODULE_ERROR_CODE_RE, Y as MODULE_NAME_RE, Z as MailAuthority, _ as MailBudgets, $ as MailClass, a0 as MailGuardConfig, a1 as MailGuardMeta, a2 as MailGuardRedis, a3 as MailPrepareInput, a4 as ModuleAppView, a5 as ModuleComposeInput, a6 as ModuleContext, a7 as ModuleDashboard, a8 as ModuleHooks, a9 as ModuleMigrations, aa as ModuleRequest, ab as ModuleResponse, ac as ModuleRouter, ad as ModuleSdk, ae as ModuleSkill, af as ModuleSlot, ag as OwnerSubmission, ah as OwnerView, ai as RECORDS_IMPORT_MAX_ROWS, aj as RecordsAuthority, ak as RecordsView, al as RouteHandler, am as RouteOptions, an as RouteRateLimit, ao as SDK_PATH, ap as SDK_TYPES_PATH, aq as SLOT_NAME_RE, ar as SubmissionsAuthority, as as buildBeaconScript, at as buildSdk, au as defineModule, av as inlineSpecifier, aw as isDefinedModule, ax as mailAppCounterKey, ay as mailBudgets, az as mailGuardConfigFromEnv, aA as memoryMailGuard, aB as memoryMailGuardRedis, aC as normalizeConfirmItems, aD as redisMailGuard, aE as respond, aF as sdkDeclarations } from './mail-guard.d-
|
|
3
|
+
import { L as Logger, M as ModuleSecretDoc, H as HookApp, E as EndUser, D as DB, R as Rule, P as Principal, A as AccessDecision, a as ModuleLimit, b as Limits, C as ConfirmRole, c as AnyModule, d as EmailMessage, e as EmailKind, f as EmailRecipient, U as UploadedFile, g as EndUserListQuery, h as EndUserPage, i as EndUserRecord, O as OwnerFilesPage, j as OwnerFile, k as RecordsCollection, l as RecordsQuery, m as RecordsPage, S as SubmissionsQuery, n as SubmissionsPage, o as MailEnvelope, p as RateLimitResult, q as MailGuard, r as ModuleAvailability, s as ModuleDashboardEditor, t as ModuleErrorDoc, u as SdkBundle, v as ModuleServices, w as EndUserCallbackInput, x as EndUserCallbackResult } from './mail-guard.d-BHuy9yy0.js';
|
|
4
|
+
export { B as BEACON_SCRIPT_PATH, y as BeaconScript, z as ComposedModuleParts, F as ConfirmContext, G as ConfirmItem, I as ConfirmedContext, J as DrobekModule, K as EndUserAuthority, N as EndUserCallbackApp, Q as FilesAuthority, T as MAIL_COUNTER_KEYS, V as MAIL_PAUSE_KEYS, W as MODULE_CONTRACT_VERSION, X as MODULE_ERROR_CODE_RE, Y as MODULE_NAME_RE, Z as MailAuthority, _ as MailBudgets, $ as MailClass, a0 as MailGuardConfig, a1 as MailGuardMeta, a2 as MailGuardRedis, a3 as MailPrepareInput, a4 as ModuleAppView, a5 as ModuleComposeInput, a6 as ModuleContext, a7 as ModuleDashboard, a8 as ModuleHooks, a9 as ModuleMigrations, aa as ModuleRequest, ab as ModuleResponse, ac as ModuleRouter, ad as ModuleSdk, ae as ModuleSkill, af as ModuleSlot, ag as OwnerSubmission, ah as OwnerView, ai as RECORDS_IMPORT_MAX_ROWS, aj as RecordsAuthority, ak as RecordsView, al as RouteHandler, am as RouteOptions, an as RouteRateLimit, ao as SDK_PATH, ap as SDK_TYPES_PATH, aq as SLOT_NAME_RE, ar as SubmissionsAuthority, as as buildBeaconScript, at as buildSdk, au as defineModule, av as inlineSpecifier, aw as isDefinedModule, ax as mailAppCounterKey, ay as mailBudgets, az as mailGuardConfigFromEnv, aA as memoryMailGuard, aB as memoryMailGuardRedis, aC as normalizeConfirmItems, aD as redisMailGuard, aE as respond, aF as sdkDeclarations } from './mail-guard.d-BHuy9yy0.js';
|
|
5
5
|
export { SdkCore } from '@drobek/sdk';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
7
|
import { Redis } from 'ioredis';
|
|
@@ -1600,5 +1600,35 @@ declare function activeModules(): Promise<ModuleSummary[]>;
|
|
|
1600
1600
|
/** Tests: install a runtime (or null to reset). */
|
|
1601
1601
|
declare function setModuleRuntimeForTests(runtime: ModuleRuntime | null): void;
|
|
1602
1602
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1603
|
+
/** Modules whose config is never copied: it points at records of the source workspace. */
|
|
1604
|
+
declare const NOT_COPIED_MODULES: readonly string[];
|
|
1605
|
+
/** The part of a saved `module` config a copy may propose, or null when nothing is left. */
|
|
1606
|
+
declare function configForCopy(module: string, config: unknown): Record<string, unknown> | null;
|
|
1607
|
+
interface DuplicateConfigsInput {
|
|
1608
|
+
sourceAppId: string;
|
|
1609
|
+
target: {
|
|
1610
|
+
id: string;
|
|
1611
|
+
slug: string;
|
|
1612
|
+
workspaceId: string;
|
|
1613
|
+
workspaceSlug: string;
|
|
1614
|
+
};
|
|
1615
|
+
actorUserId: string;
|
|
1616
|
+
surface: 'mcp' | 'web';
|
|
1617
|
+
}
|
|
1618
|
+
interface DuplicateConfigsResult {
|
|
1619
|
+
applied: string[];
|
|
1620
|
+
pending: {
|
|
1621
|
+
module: string;
|
|
1622
|
+
changes: string[];
|
|
1623
|
+
confirm_url?: string;
|
|
1624
|
+
}[];
|
|
1625
|
+
skipped: {
|
|
1626
|
+
module: string;
|
|
1627
|
+
reason: 'not_copied' | 'not_enabled' | 'invalid';
|
|
1628
|
+
}[];
|
|
1629
|
+
}
|
|
1630
|
+
/** Propose the source app's saved module configs to the copy (see the file comment). */
|
|
1631
|
+
declare function duplicateModuleConfigs(runtime: ModuleRuntime, input: DuplicateConfigsInput): Promise<DuplicateConfigsResult>;
|
|
1632
|
+
|
|
1633
|
+
export { AUTH_PROVIDER_ID_RE, AUTH_RESERVED_CONFIG_KEYS, AccessDecision, AnyModule, BACKEND_IMPORT_SKILLS, CORE_ERROR_CODES, CORE_LIMITS, ConfirmRole, DB, DEFAULT_MAX_BODY_BYTES, DEFAULT_MODULES_DIR, EMAIL_PROVIDER_ID, EMAIL_RE, END_USER_COOKIE, END_USER_COOKIE_INSECURE, END_USER_SESSION_TTL_SEC, END_USER_TOKEN_RE, EmailKind, EmailMessage, EmailRecipient, EndUser, EndUserCallbackInput, EndUserCallbackResult, EndUserListQuery, EndUserPage, EndUserRecord, HookApp, LIMITS_CACHE_TTL_SEC, LIMITS_SIGNATURE_HEADER, LIMITS_TIMESTAMP_HEADER, Limits, Logger, Lru, MAX_EMAIL_TEXT, MAX_FILE_HEAD_BYTES, MODULE_ERROR_CODES, MailEnvelope, MailGuard, ModuleAvailability, ModuleDashboardEditor, ModuleError, ModuleErrorDoc, ModuleLimit, ModuleLoadError, ModuleRuntime, ModuleSecretDoc, ModuleServices, NOT_COPIED_MODULES, OwnerFile, OwnerFilesPage, PENDING_MAIL_WINDOW_MS, PLATFORM_SKILL_NAME, Principal, RESERVED_MODULE_NAMES, RULE_TOKENS, RateLimitResult, RecordsCollection, RecordsPage, RecordsQuery, Rule, SDK_HEADER, SECRET_MAX_BYTES, SECRET_NAME_RE, SdkBundle, SecretStoreError, SubmissionsPage, SubmissionsQuery, UploadedFile, activeModules, appOwnerEmails, authIdentitySchema, authProviderSchema, authSignedInObserverSchema, capEmailText, checkErrorCodes, checkModuleSet, checkRequires, collectContributions, composeModule, configForCopy, confirmUrl, cookiePrincipalResolver, createEndUserSession, createLimitsProvider, csvChunks, decideAccess, defineAuthProvider, defineSignInObserver, deleteModuleSecret, destroyEndUserSession, duplicateModuleConfigs, effectiveConfigDefaults, emailKind, endUserAuthorityOf, endUserCookieHeader, endUserCookieName, endUserCookiesSecure, endUserEpochKey, endUserSessionKey, filesAuthorityOf, generalSkillsDir, getModuleSecret, hasControlBytes, isModuleError, isReadable, isValidRule, issuePaths, jsonEqual, jsonKey, limitsProviderConfigError, loadEndUserSession, loadGeneralSkills, loadModuleRuntime, loadModuleSet, loadModules, looksLikeSvg, mailAuthorityOf, memoryRateLimiter, mergePatch, moduleDefaultsEnvName, moduleEnabledLimitName, moduleJournalTable, moduleNotEnabled, moduleRuntime, multipartBoundary, packageNameFor, parseEndUserSession, parseModuleList, parseMultipart, parseRule, parseSkillMarkdown, pendingMail, pendingMailKey, perIpLimitKey, readConfigRow, readEndUserToken, recipientRefs, recordsAuthorityOf, redisRateLimiter, renewEndUserSession, resolveRecipients, revokeEndUserSessions, ruleIsPublic, sanitizeSubject, secretsSet, secretsStatus, setModuleRuntimeForTests, setModuleSecret, signLimitsRequest, skillForImport, skillHint, smtpEmailTransport, sniffSignature, stableJson, streamMultipartFile, submissionsAuthorityOf };
|
|
1634
|
+
export type { AppModuleState, AuthIdentity, AuthProvider, AuthProviderBeginInput, AuthProviderBeginResult, AuthProviderCallbackInput, AuthProviderSecretDoc, AuthProviderSecrets, AuthSignInEvent, AuthSignedInObserver, BoundEndUsers, BoundFiles, BoundRecords, BoundSubmissions, ConfigRow, ConfigureInput, ConfigureResult, CurrentEndUser, DecisionInput, DuplicateConfigsInput, DuplicateConfigsResult, EmailTransport, EndUserRedis, EndUserSession, LimitsProvider, LoadRuntimeOptions, LoadedModules, ModuleDashboardView, ModuleErrorBody, ModuleErrorCode, ModuleErrorSection, ModuleFacts, ModuleOrigin, ModuleSource, ModuleSummary, PendingChange, PendingMailModule, PendingView, PipelineRequest, PipelineResult, PlatformApp, PlatformRequest, PrincipalResolver, RateLimiter, RecipientSources, RuntimeDeps, SkillEntry, SkillInfo, SkillListItem, SlotContribution, SniffedType, TransportMessage, WorkspaceModuleSource, WorkspaceModuleState };
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
endUserSessionKey,
|
|
67
67
|
filesAuthorityOf,
|
|
68
68
|
generalSkillsDir,
|
|
69
|
+
getDb,
|
|
69
70
|
getModuleSecret,
|
|
70
71
|
hasControlBytes,
|
|
71
72
|
inlineSpecifier,
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
91
92
|
memoryMailGuardRedis,
|
|
92
93
|
memoryRateLimiter,
|
|
93
94
|
mergePatch,
|
|
95
|
+
moduleConfigs,
|
|
94
96
|
moduleDefaultsEnvName,
|
|
95
97
|
moduleEnabledLimitName,
|
|
96
98
|
moduleJournalTable,
|
|
@@ -132,7 +134,7 @@ import {
|
|
|
132
134
|
stableJson,
|
|
133
135
|
streamMultipartFile,
|
|
134
136
|
submissionsAuthorityOf
|
|
135
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-B7PEIWK7.js";
|
|
136
138
|
|
|
137
139
|
// packages/modules/dist/index.js
|
|
138
140
|
import { z as z2 } from "zod";
|
|
@@ -227,6 +229,63 @@ async function* csvChunks(lines, chunkChars = CSV_CHUNK_CHARS) {
|
|
|
227
229
|
if (chunk)
|
|
228
230
|
yield chunk;
|
|
229
231
|
}
|
|
232
|
+
|
|
233
|
+
// packages/modules/dist/duplicate.js
|
|
234
|
+
import { eq } from "drizzle-orm";
|
|
235
|
+
var NOT_COPIED_MODULES = ["proxy"];
|
|
236
|
+
var EMAIL_RE2 = /[^\s@<>"',;]+@[^\s@<>"',;]+\.[^\s@<>"',;]+/;
|
|
237
|
+
function stripForCopy(value) {
|
|
238
|
+
if (value === null)
|
|
239
|
+
return void 0;
|
|
240
|
+
if (typeof value === "string")
|
|
241
|
+
return EMAIL_RE2.test(value) ? void 0 : value;
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
return value.map(stripForCopy).filter((v) => v !== void 0);
|
|
244
|
+
}
|
|
245
|
+
if (typeof value === "object") {
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const [k, v] of Object.entries(value)) {
|
|
248
|
+
const kept = stripForCopy(v);
|
|
249
|
+
if (kept !== void 0)
|
|
250
|
+
out[k] = kept;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
function configForCopy(module, config) {
|
|
257
|
+
if (NOT_COPIED_MODULES.includes(module))
|
|
258
|
+
return null;
|
|
259
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
260
|
+
return null;
|
|
261
|
+
const out = stripForCopy(config);
|
|
262
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
263
|
+
}
|
|
264
|
+
async function duplicateModuleConfigs(runtime, input) {
|
|
265
|
+
const rows = await getDb().select({ module: moduleConfigs.module, config: moduleConfigs.config }).from(moduleConfigs).where(eq(moduleConfigs.appId, input.sourceAppId)).orderBy(moduleConfigs.module);
|
|
266
|
+
const out = { applied: [], pending: [], skipped: [] };
|
|
267
|
+
for (const row of rows) {
|
|
268
|
+
const patch = configForCopy(row.module, row.config);
|
|
269
|
+
if (!patch) {
|
|
270
|
+
if (NOT_COPIED_MODULES.includes(row.module))
|
|
271
|
+
out.skipped.push({ module: row.module, reason: "not_copied" });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const r = await runtime.configure({ app: input.target, module: row.module, patch, actorUserId: input.actorUserId, surface: input.surface });
|
|
276
|
+
if (r.pending_confirmation.length > 0) {
|
|
277
|
+
out.pending.push({ module: row.module, changes: r.pending_confirmation, ...r.confirm_url ? { confirm_url: r.confirm_url } : {} });
|
|
278
|
+
} else {
|
|
279
|
+
out.applied.push(row.module);
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!isModuleError(err))
|
|
283
|
+
throw err;
|
|
284
|
+
out.skipped.push({ module: row.module, reason: err.code === "module_not_enabled" || err.code === "not_found" ? "not_enabled" : "invalid" });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
230
289
|
export {
|
|
231
290
|
AUTH_PROVIDER_ID_RE,
|
|
232
291
|
AUTH_RESERVED_CONFIG_KEYS,
|
|
@@ -257,6 +316,7 @@ export {
|
|
|
257
316
|
ModuleError,
|
|
258
317
|
ModuleLoadError,
|
|
259
318
|
ModuleRuntime,
|
|
319
|
+
NOT_COPIED_MODULES,
|
|
260
320
|
PENDING_MAIL_WINDOW_MS,
|
|
261
321
|
PLATFORM_SKILL_NAME,
|
|
262
322
|
RECORDS_IMPORT_MAX_ROWS,
|
|
@@ -282,6 +342,7 @@ export {
|
|
|
282
342
|
checkRequires,
|
|
283
343
|
collectContributions,
|
|
284
344
|
composeModule,
|
|
345
|
+
configForCopy,
|
|
285
346
|
confirmUrl,
|
|
286
347
|
cookiePrincipalResolver,
|
|
287
348
|
createEndUserSession,
|
|
@@ -293,6 +354,7 @@ export {
|
|
|
293
354
|
defineSignInObserver,
|
|
294
355
|
deleteModuleSecret,
|
|
295
356
|
destroyEndUserSession,
|
|
357
|
+
duplicateModuleConfigs,
|
|
296
358
|
effectiveConfigDefaults,
|
|
297
359
|
emailKind,
|
|
298
360
|
endUserAuthorityOf,
|
|
@@ -185,9 +185,16 @@ declare const apps = pgTable(
|
|
|
185
185
|
galleryDescription: text('gallery_description'),
|
|
186
186
|
/** NSO-340: a super-admin hid the gallery entry (non-null = hidden, whatever the owner sets). */
|
|
187
187
|
galleryHiddenAt: timestamp('gallery_hidden_at'),
|
|
188
|
+
/** NSO-340: the owner lets signed-in people duplicate the listed app into their own workspace. */
|
|
189
|
+
galleryAllowDuplicate: boolean('gallery_allow_duplicate').notNull().default(false),
|
|
190
|
+
/** NSO-340: the gallery app this one was duplicated from (null when the source is gone or none). */
|
|
191
|
+
duplicatedFromAppId: text('duplicated_from_app_id').references((): AnyPgColumn => apps.id, { onDelete: 'set null' }),
|
|
192
|
+
/** The source's slug at duplication time, kept when the source is deleted. */
|
|
193
|
+
duplicatedFromSlug: text('duplicated_from_slug'),
|
|
188
194
|
},
|
|
189
195
|
(t) => [
|
|
190
196
|
uniqueIndex('apps_slug_uq').on(t.slug),
|
|
197
|
+
index('apps_duplicated_from_idx').on(t.duplicatedFromAppId).where(sql`${t.duplicatedFromAppId} IS NOT NULL`),
|
|
191
198
|
// The public gallery page reads listed apps newest-published first (NSO-340).
|
|
192
199
|
index('apps_gallery_idx').on(t.publishedAt.desc(), t.slug.desc()).where(sql`${t.galleryListed}`),
|
|
193
200
|
index('apps_workspace_idx').on(t.workspaceId),
|
|
@@ -202,6 +209,34 @@ declare const apps = pgTable(
|
|
|
202
209
|
]
|
|
203
210
|
);
|
|
204
211
|
|
|
212
|
+
/** NSO-340: a signed-in drobek account likes a gallery app — one row per account and app. */
|
|
213
|
+
declare const galleryLikes = pgTable(
|
|
214
|
+
'gallery_likes',
|
|
215
|
+
{
|
|
216
|
+
appId: text('app_id')
|
|
217
|
+
.notNull()
|
|
218
|
+
.references(() => apps.id, { onDelete: 'cascade' }),
|
|
219
|
+
userId: text('user_id')
|
|
220
|
+
.notNull()
|
|
221
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
222
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
223
|
+
},
|
|
224
|
+
(t) => [primaryKey({ columns: [t.appId, t.userId] })]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/** NSO-340: opens of a gallery app through the gallery's counting link, per UTC day (`YYYY-MM-DD`); no visitor data. */
|
|
228
|
+
declare const galleryOpens = pgTable(
|
|
229
|
+
'gallery_opens',
|
|
230
|
+
{
|
|
231
|
+
appId: text('app_id')
|
|
232
|
+
.notNull()
|
|
233
|
+
.references(() => apps.id, { onDelete: 'cascade' }),
|
|
234
|
+
day: text('day').notNull(),
|
|
235
|
+
count: integer('count').notNull().default(0),
|
|
236
|
+
},
|
|
237
|
+
(t) => [primaryKey({ columns: [t.appId, t.day] })]
|
|
238
|
+
);
|
|
239
|
+
|
|
205
240
|
/** Content-addressed file bytes, shared by every version (and app) that uses them. */
|
|
206
241
|
declare const blobs = pgTable('blobs', {
|
|
207
242
|
sha256: text('sha256').primaryKey(),
|
|
@@ -861,6 +896,8 @@ declare const schema_blobs: typeof blobs;
|
|
|
861
896
|
declare const schema_bytea: typeof bytea;
|
|
862
897
|
declare const schema_compileStatusEnum: typeof compileStatusEnum;
|
|
863
898
|
declare const schema_domains: typeof domains;
|
|
899
|
+
declare const schema_galleryLikes: typeof galleryLikes;
|
|
900
|
+
declare const schema_galleryOpens: typeof galleryOpens;
|
|
864
901
|
declare const schema_membershipRoleEnum: typeof membershipRoleEnum;
|
|
865
902
|
declare const schema_memberships: typeof memberships;
|
|
866
903
|
declare const schema_moduleConfigs: typeof moduleConfigs;
|
|
@@ -900,6 +937,8 @@ declare namespace schema {
|
|
|
900
937
|
schema_bytea as bytea,
|
|
901
938
|
schema_compileStatusEnum as compileStatusEnum,
|
|
902
939
|
schema_domains as domains,
|
|
940
|
+
schema_galleryLikes as galleryLikes,
|
|
941
|
+
schema_galleryOpens as galleryOpens,
|
|
903
942
|
schema_membershipRoleEnum as membershipRoleEnum,
|
|
904
943
|
schema_memberships as memberships,
|
|
905
944
|
schema_moduleConfigs as moduleConfigs,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- NSO-340: duplicating a gallery app. The owner opts in per app
|
|
2
|
+
-- (gallery_allow_duplicate); a copy remembers its source (id, and the slug
|
|
3
|
+
-- for when the source is deleted). Additive only.
|
|
4
|
+
ALTER TABLE "apps" ADD COLUMN "gallery_allow_duplicate" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
5
|
+
ALTER TABLE "apps" ADD COLUMN "duplicated_from_app_id" text;--> statement-breakpoint
|
|
6
|
+
ALTER TABLE "apps" ADD COLUMN "duplicated_from_slug" text;--> statement-breakpoint
|
|
7
|
+
ALTER TABLE "apps" ADD CONSTRAINT "apps_duplicated_from_app_id_apps_id_fk" FOREIGN KEY ("duplicated_from_app_id") REFERENCES "public"."apps"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
8
|
+
CREATE INDEX "apps_duplicated_from_idx" ON "apps" USING btree ("duplicated_from_app_id") WHERE "apps"."duplicated_from_app_id" IS NOT NULL;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- NSO-340: gallery likes (one per signed-in account and app) and opens through
|
|
2
|
+
-- the gallery's counting link (per app and UTC day, no visitor data).
|
|
3
|
+
CREATE TABLE "gallery_likes" (
|
|
4
|
+
"app_id" text NOT NULL,
|
|
5
|
+
"user_id" text NOT NULL,
|
|
6
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
7
|
+
CONSTRAINT "gallery_likes_app_id_user_id_pk" PRIMARY KEY("app_id","user_id")
|
|
8
|
+
);
|
|
9
|
+
--> statement-breakpoint
|
|
10
|
+
CREATE TABLE "gallery_opens" (
|
|
11
|
+
"app_id" text NOT NULL,
|
|
12
|
+
"day" text NOT NULL,
|
|
13
|
+
"count" integer DEFAULT 0 NOT NULL,
|
|
14
|
+
CONSTRAINT "gallery_opens_app_id_day_pk" PRIMARY KEY("app_id","day")
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
ALTER TABLE "gallery_likes" ADD CONSTRAINT "gallery_likes_app_id_apps_id_fk" FOREIGN KEY ("app_id") REFERENCES "public"."apps"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
18
|
+
ALTER TABLE "gallery_likes" ADD CONSTRAINT "gallery_likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
19
|
+
ALTER TABLE "gallery_opens" ADD CONSTRAINT "gallery_opens_app_id_apps_id_fk" FOREIGN KEY ("app_id") REFERENCES "public"."apps"("id") ON DELETE cascade ON UPDATE no action;
|
|
@@ -162,6 +162,20 @@
|
|
|
162
162
|
"when": 1790496410618,
|
|
163
163
|
"tag": "0027_publish_block",
|
|
164
164
|
"breakpoints": true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"idx": 28,
|
|
168
|
+
"version": "7",
|
|
169
|
+
"when": 1790525749936,
|
|
170
|
+
"tag": "0028_gallery_duplicate",
|
|
171
|
+
"breakpoints": true
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"idx": 29,
|
|
175
|
+
"version": "7",
|
|
176
|
+
"when": 1790525800000,
|
|
177
|
+
"tag": "0029_gallery_likes",
|
|
178
|
+
"breakpoints": true
|
|
165
179
|
}
|
|
166
180
|
]
|
|
167
181
|
}
|
package/dist/testing.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runRoute,
|
|
24
24
|
sanitizeSubject,
|
|
25
25
|
workspaces
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-B7PEIWK7.js";
|
|
27
27
|
|
|
28
28
|
// packages/modules/dist/testing.js
|
|
29
29
|
import { existsSync } from "node:fs";
|
|
@@ -63,14 +63,28 @@ var TOOL_DOCS = [
|
|
|
63
63
|
returns: "{ app_id, name, slug, workspace, version:1, compile:{ok,errors,warnings}, preview_url, briefing, skills:[{name,use_when}] }",
|
|
64
64
|
example: { name: "Shift planner", template: "react-ts" }
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: "duplicate_app",
|
|
68
|
+
title: "Duplicate a gallery app",
|
|
69
|
+
scope: "write (editor+ role in the target workspace)",
|
|
70
|
+
description: "Copy an app from this server's public gallery into a workspace of the user \u2014 only an app whose owner allows duplicates (the gallery shows it as duplicable). Call it when the user asks to copy, fork or duplicate a gallery app. The copy is a new, unpublished app whose version 1 holds the source's PUBLISHED files, and it remembers where it came from (get_app `duplicated_from`). The source's module settings are proposed to the copy through the normal confirmation flow: anything that needs a confirmation waits on the new app's Modules page (`modules.pending[].confirm_url` \u2014 tell the user), and e-mail addresses and proxy upstreams are dropped. Never copied: secrets, data, end users, uploads, app assets, domains and the gallery listing. Refused when the server has no gallery (gallery_disabled), the app is not in the gallery (not_found), its owner does not allow copies (not_duplicable), the user made DUPLICATES_PER_USER_HOUR copies within the last hour (rate_limited) or the workspace is full (limit_exceeded); a `from` address that is not this server's is invalid_params. The same copy is on the dashboard at /duplicate/<slug>.",
|
|
71
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
72
|
+
fields: [
|
|
73
|
+
{ name: "from", type: "string", required: true, description: "The gallery app on this server: its slug, its address (published or --preview host, or a verified custom domain) or this dashboard's /duplicate/<slug> URL. An address of another server is invalid_params." },
|
|
74
|
+
{ name: "workspace", type: "string (optional)", required: false, description: "Workspace slug for the copy (editor+); defaults to your personal workspace." },
|
|
75
|
+
{ name: "name", type: "string (optional, \u2264 80 chars)", required: false, description: 'Name of the copy; default "<name> copy". The slug is derived from it.' }
|
|
76
|
+
],
|
|
77
|
+
returns: "{ app_id, slug, workspace, version:1, from, preview_url, modules:{ applied:[module], pending:[{module,changes,confirm_url}], skipped:[{module,reason}] }, note? }",
|
|
78
|
+
example: { from: "pixel-wall", name: "My pixel wall" }
|
|
79
|
+
},
|
|
66
80
|
{
|
|
67
81
|
name: "get_app",
|
|
68
82
|
title: "Get an app",
|
|
69
83
|
scope: "read (any role in the workspace)",
|
|
70
|
-
description: "Snapshot of one app: everything list_apps shows plus the briefing, the source files of the latest version ({path,size,sha256}), the last 20 versions (number, created_at, actor_kind, reasoning, compile_status), the latest compile errors, the platform modules (per module: whether it is enabled for the app's workspace \u2014 an opt-in module the operator has not enabled says enabled:false and cannot be used \u2014, its effective config, whether a change waits for the owner's confirmation, which secrets are set \u2014 names and hasSecret only, never values \u2014 and the module's info, e.g. proxy: the workspace upstreams with registered/assigned/call/hasSecret), the skills list (without the opt-in modules that are off for the workspace), the public gallery state (listed, description, hidden_by_admin, visible \u2014 or enabled:false when the server has no gallery), the custom domains in short (host, status pending | verified, primary \u2014 list_domains has their DNS records), `can_publish` (+ `publish_contact` when the workspace may not publish: the operator blocked it or has not approved it yet) and the workspace's `publishing` state (default | allowed | blocked), and the write lock (holder + expires_at) if someone holds it. Use it to re-orient before editing.",
|
|
84
|
+
description: "Snapshot of one app: everything list_apps shows plus the briefing, the source files of the latest version ({path,size,sha256}), the last 20 versions (number, created_at, actor_kind, reasoning, compile_status), the latest compile errors, the platform modules (per module: whether it is enabled for the app's workspace \u2014 an opt-in module the operator has not enabled says enabled:false and cannot be used \u2014, its effective config, whether a change waits for the owner's confirmation, which secrets are set \u2014 names and hasSecret only, never values \u2014 and the module's info, e.g. proxy: the workspace upstreams with registered/assigned/call/hasSecret), the skills list (without the opt-in modules that are off for the workspace), the public gallery state (listed, description, hidden_by_admin, visible, allow_duplicate, likes \u2014 signed-in accounts that like it \u2014 and opens through the gallery in the last 30 days; or enabled:false when the server has no gallery), `duplicated_from` (the gallery app this one was copied from, when it was), the custom domains in short (host, status pending | verified, primary \u2014 list_domains has their DNS records), `can_publish` (+ `publish_contact` when the workspace may not publish: the operator blocked it or has not approved it yet) and the workspace's `publishing` state (default | allowed | blocked), and the write lock (holder + expires_at) if someone holds it. Use it to re-orient before editing.",
|
|
71
85
|
annotations: READ_ONLY,
|
|
72
86
|
fields: [{ name: "app_id", type: "string", required: true, description: "The app id (from list_apps / create_app)." }],
|
|
73
|
-
returns: '{ app_id, name, slug, workspace, preview_url, published_url?, published_version?, latest_version, compile_status, compile_errors, briefing, files:[{path,size,sha256}], versions:[{number,created_at,actor_kind,reasoning,compile_status}], modules:{<name>:{enabled,configured,config,pending,pending_confirmation?,confirm_url?,secrets?:[{name,hasSecret}],info?}}, skills:[{name,use_when}], gallery:{enabled,listed?,description?,hidden_by_admin?,visible?}, domains:[{host,status:"pending"|"verified",primary}], can_publish, publish_contact?, publishing, lock?:{holder,expires_at}, locked_by_admin?, locked_reason? }',
|
|
87
|
+
returns: '{ app_id, name, slug, workspace, preview_url, published_url?, published_version?, latest_version, compile_status, compile_errors, briefing, files:[{path,size,sha256}], versions:[{number,created_at,actor_kind,reasoning,compile_status}], modules:{<name>:{enabled,configured,config,pending,pending_confirmation?,confirm_url?,secrets?:[{name,hasSecret}],info?}}, skills:[{name,use_when}], gallery:{enabled,listed?,description?,hidden_by_admin?,visible?,allow_duplicate?,likes?,opens?}, duplicated_from?, domains:[{host,status:"pending"|"verified",primary}], can_publish, publish_contact?, publishing, lock?:{holder,expires_at}, locked_by_admin?, locked_reason? }',
|
|
74
88
|
example: { app_id: "k3v9x0\u2026" }
|
|
75
89
|
},
|
|
76
90
|
{
|
|
@@ -148,7 +162,7 @@ var TOOL_DOCS = [
|
|
|
148
162
|
name: "set_gallery_listing",
|
|
149
163
|
title: "List an app in the public gallery",
|
|
150
164
|
scope: "publish (editor+ role in the workspace)",
|
|
151
|
-
description: "Show a published app in this server's public gallery (its name, a one- or two-sentence description and its production URL, visible to everyone), change that description, or take the app out of the gallery. Listing (`listed: true`) needs a published app, a plain-text `description` of at most 160 characters and `user_confirmed: true` \u2014 set it ONLY after the user explicitly said yes to exactly this listing: ask them first and show them the description. Never list an app on your own initiative. Without the confirmation the answer is user_confirmation_required and nothing changes. Unlisting (`listed: false`) needs no confirmation and works at once. Refused when the server has no gallery (gallery_disabled), when the app is not published (not_published) and when the server operator hid the app from the gallery (gallery_hidden). Unpublishing the app also takes it out of the gallery. get_app shows the current state (`gallery`).",
|
|
165
|
+
description: "Show a published app in this server's public gallery (its name, a one- or two-sentence description and its production URL, visible to everyone), change that description, or take the app out of the gallery. Listing (`listed: true`) needs a published app, a plain-text `description` of at most 160 characters and `user_confirmed: true` \u2014 set it ONLY after the user explicitly said yes to exactly this listing: ask them first and show them the description. Never list an app on your own initiative. Without the confirmation the answer is user_confirmation_required and nothing changes. Unlisting (`listed: false`) needs no confirmation and works at once. Refused when the server has no gallery (gallery_disabled), when the app is not published (not_published) and when the server operator hid the app from the gallery (gallery_hidden). With `allow_duplicate: true` the gallery also offers a Duplicate button: signed-in people copy the published files into their own workspace (never data, users, secrets or domains) \u2014 include that in the question to the user. Unpublishing the app also takes it out of the gallery. get_app shows the current state (`gallery`).",
|
|
152
166
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
153
167
|
fields: [
|
|
154
168
|
{ name: "app_id", type: "string", required: true, description: "The app id." },
|
|
@@ -159,6 +173,12 @@ var TOOL_DOCS = [
|
|
|
159
173
|
required: false,
|
|
160
174
|
description: "The public description: plain text, one or two sentences."
|
|
161
175
|
},
|
|
176
|
+
{
|
|
177
|
+
name: "allow_duplicate",
|
|
178
|
+
type: "boolean (listing only, optional)",
|
|
179
|
+
required: false,
|
|
180
|
+
description: "true lets signed-in people copy the published app into their own workspace (duplicate_app, the gallery's Duplicate button); omitted keeps the current choice. Covered by the same user_confirmed."
|
|
181
|
+
},
|
|
162
182
|
{
|
|
163
183
|
name: "user_confirmed",
|
|
164
184
|
type: "boolean (listing only)",
|
|
@@ -166,7 +186,7 @@ var TOOL_DOCS = [
|
|
|
166
186
|
description: "true ONLY after the user explicitly said yes to this listing and description."
|
|
167
187
|
}
|
|
168
188
|
],
|
|
169
|
-
returns: "{ app_id, listed, description, changed, visible, note? }",
|
|
189
|
+
returns: "{ app_id, listed, description, allow_duplicate, changed, visible, note? }",
|
|
170
190
|
example: { app_id: "k3v9x0\u2026", listed: true, description: "Plan weekly shifts for a small team.", user_confirmed: true }
|
|
171
191
|
},
|
|
172
192
|
{
|
|
@@ -340,6 +360,48 @@ var TOOL_DOCS = [
|
|
|
340
360
|
returns: "{ removed:host, was_verified, was_primary, note }",
|
|
341
361
|
example: { app_id: "k3v9x0\u2026", host: "shop.example.com", user_confirmed: true }
|
|
342
362
|
},
|
|
363
|
+
{
|
|
364
|
+
name: "list_upstreams",
|
|
365
|
+
title: "List a workspace's proxy upstreams",
|
|
366
|
+
scope: "read (workspace-admin role in the workspace)",
|
|
367
|
+
description: "The external APIs registered for the proxy module in one workspace \u2014 the dashboard's workspace \u2192 Upstreams page: per upstream its `name` (what apps call it by), `base_url`, `allowed_methods`, `allowed_path_prefixes`, `auth_type` (none | bearer | header), `auth_header_name`, `has_secret` (never the secret) and `apps` \u2014 the apps whose assignment a workspace admin confirmed. Plus `upstreams_url`, the dashboard page. Workspace admins only (forbidden otherwise); get_app \u2192 modules.proxy.info shows any editor what one app may call. Read-only.",
|
|
368
|
+
annotations: READ_ONLY,
|
|
369
|
+
fields: [{ name: "workspace", type: "string", required: true, description: "The workspace slug." }],
|
|
370
|
+
returns: '{ workspace, upstreams:[{ name, base_url, allowed_methods, allowed_path_prefixes, auth_type:"none"|"bearer"|"header", auth_header_name, has_secret, apps:[slug], created_at }], upstreams_url }',
|
|
371
|
+
example: { workspace: "acme-crew" }
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "register_upstream",
|
|
375
|
+
title: "Register a proxy upstream",
|
|
376
|
+
scope: "write (workspace-admin role in the workspace)",
|
|
377
|
+
description: 'Register an external API for the proxy module in a workspace \u2014 what the dashboard\'s Upstreams page does, with the same checks: a public https or http base URL on port 80/443 (never a private address), the allowed HTTP methods and path prefixes apps may call under it. `auth_type: "none"` (an API without a key, e.g. https://pokeapi.co) registers at once. `bearer` / `header` need a key, and a key never passes through MCP: the answer is `registered: false` with `secret_url` \u2014 the Upstreams page with every field filled in; give the user that link, they paste the key and click Register. Never ask for the key in chat. A registered upstream does nothing yet: assign it to an app with configure_module(\'proxy\', { upstreams: { <name>: { rules: { call: "user" } } } }) \u2014 a workspace admin confirms that in the dashboard. A name the workspace already has answers upstream_already_registered.',
|
|
378
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
379
|
+
fields: [
|
|
380
|
+
{ name: "workspace", type: "string", required: true, description: "The workspace slug." },
|
|
381
|
+
{ name: "name", type: "string", required: true, description: "The name apps call it by, e.g. pokeapi." },
|
|
382
|
+
{ name: "base_url", type: "string", required: true, description: "The base URL, e.g. https://pokeapi.co." },
|
|
383
|
+
{ name: "allowed_methods", type: "string[]", required: true, description: 'e.g. ["GET"].' },
|
|
384
|
+
{ name: "allowed_path_prefixes", type: "string[]", required: true, description: 'e.g. ["/api/v2/"].' },
|
|
385
|
+
{ name: "auth_type", type: '"none" | "bearer" | "header"', required: true, description: "none registers now; bearer / header answer secret_url." },
|
|
386
|
+
{ name: "auth_header_name", type: "string (header only)", required: false, description: "The header that carries the key, e.g. X-Api-Key." }
|
|
387
|
+
],
|
|
388
|
+
returns: "{ registered:true, upstream:{ name, base_url, allowed_methods, allowed_path_prefixes, auth_type, auth_header_name, has_secret, apps, created_at }, next } \u2014 or { registered:false, name, secret_url, note } for bearer / header",
|
|
389
|
+
example: { workspace: "acme-crew", name: "pokeapi", base_url: "https://pokeapi.co", allowed_methods: ["GET"], allowed_path_prefixes: ["/api/v2/"], auth_type: "none" }
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "remove_upstream",
|
|
393
|
+
title: "Remove a proxy upstream",
|
|
394
|
+
scope: "write (workspace-admin role in the workspace)",
|
|
395
|
+
description: "Delete a registered upstream and its stored key \u2014 the dashboard's Delete on the Upstreams page. Every app that calls it gets 404 upstream_not_registered at once, so it needs `user_confirmed: true` \u2014 set it ONLY after the user explicitly said yes to removing exactly this upstream; without it the answer is user_confirmation_required with the `apps` that use it, and nothing changes. Registering the name again creates a new record: each app's assignment then needs a new confirmation.",
|
|
396
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
397
|
+
fields: [
|
|
398
|
+
{ name: "workspace", type: "string", required: true, description: "The workspace slug." },
|
|
399
|
+
{ name: "name", type: "string", required: true, description: "A registered upstream (list_upstreams lists them)." },
|
|
400
|
+
{ name: "user_confirmed", type: "boolean", required: false, description: "true ONLY after the user explicitly said yes to removing it." }
|
|
401
|
+
],
|
|
402
|
+
returns: "{ removed:name, apps:[slug], note }",
|
|
403
|
+
example: { workspace: "acme-crew", name: "pokeapi", user_confirmed: true }
|
|
404
|
+
},
|
|
343
405
|
{
|
|
344
406
|
name: "set_workspace_publishing",
|
|
345
407
|
title: "Set a workspace's publishing",
|
|
@@ -362,6 +424,16 @@ var TOOL_DOCS = [
|
|
|
362
424
|
];
|
|
363
425
|
var TOOL_NAMES = TOOL_DOCS.map((t) => t.name);
|
|
364
426
|
|
|
427
|
+
// packages/agent-dx/dist/plugin.js
|
|
428
|
+
var PLUGIN_REPO = "freema/drobek-plugin";
|
|
429
|
+
var PLUGIN_REPO_URL = `https://github.com/${PLUGIN_REPO}`;
|
|
430
|
+
var PLUGIN_MARKETPLACE = "drobek";
|
|
431
|
+
var PLUGIN_NAME = "drobek";
|
|
432
|
+
var PLUGIN_MARKETPLACE_ADD_COMMAND = `claude plugin marketplace add ${PLUGIN_REPO}`;
|
|
433
|
+
var PLUGIN_INSTALL_COMMAND = `claude plugin install ${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;
|
|
434
|
+
var PLUGIN_BUILD_COMMAND = `/${PLUGIN_NAME}:build-app`;
|
|
435
|
+
var PLUGIN_PORT_COMMAND = `/${PLUGIN_NAME}:port-artifact`;
|
|
436
|
+
|
|
365
437
|
// packages/agent-dx/dist/errors-catalogue.js
|
|
366
438
|
var ERROR_CATALOGUE = [
|
|
367
439
|
// ── MCP tools (isError: true, body { code, message, hint }) ───────────────
|
|
@@ -463,10 +535,16 @@ var ERROR_CATALOGUE = [
|
|
|
463
535
|
},
|
|
464
536
|
{
|
|
465
537
|
code: "gallery_disabled",
|
|
466
|
-
surface: "MCP tool isError (set_gallery_listing)",
|
|
538
|
+
surface: "MCP tool isError (set_gallery_listing, duplicate_app)",
|
|
467
539
|
meaning: "This server runs no public gallery (its operator left GALLERY_ENABLED off). Nothing changed.",
|
|
468
540
|
fix: "Tell the user this server has no public gallery; do not retry."
|
|
469
541
|
},
|
|
542
|
+
{
|
|
543
|
+
code: "not_duplicable",
|
|
544
|
+
surface: "MCP tool isError (duplicate_app); dashboard 403 (duplicate page)",
|
|
545
|
+
meaning: "The app is in the public gallery, but its owner did not allow duplicates. Nothing was created.",
|
|
546
|
+
fix: "Do not retry and do not copy the app another way. Tell the user its owner does not allow copies; they can build their own app with create_app."
|
|
547
|
+
},
|
|
470
548
|
{
|
|
471
549
|
code: "asset_too_large",
|
|
472
550
|
surface: "MCP tool isError (create_asset_upload); upload URL 413",
|
|
@@ -545,6 +623,12 @@ var ERROR_CATALOGUE = [
|
|
|
545
623
|
meaning: "A DNS lookup timed out or the resolver failed (SERVFAIL, network), so the records could not be checked. Nothing changed \u2014 an existing verification is kept.",
|
|
546
624
|
fix: "Try verify_domain again in a few minutes; if it keeps failing, the domain's DNS servers may be down \u2014 tell the user."
|
|
547
625
|
},
|
|
626
|
+
{
|
|
627
|
+
code: "upstream_already_registered",
|
|
628
|
+
surface: "MCP tool isError (register_upstream)",
|
|
629
|
+
meaning: "The workspace already has an upstream with this name. Nothing changed.",
|
|
630
|
+
fix: "list_upstreams shows it; use it as it is, pick another name, or remove_upstream it first (the user confirms) and register it again."
|
|
631
|
+
},
|
|
548
632
|
{
|
|
549
633
|
code: "internal_error",
|
|
550
634
|
surface: "MCP tool isError",
|
|
@@ -611,9 +695,9 @@ var ERROR_CATALOGUE = [
|
|
|
611
695
|
},
|
|
612
696
|
{
|
|
613
697
|
code: "rate_limited",
|
|
614
|
-
surface: "module route 429 (DrobekError), Retry-After; MCP tool isError (create_asset_upload)",
|
|
615
|
-
meaning: "A module limit was hit (per visitor, per user or per app \u2014 `details.limit` per `details.window_seconds`). From create_asset_upload: the app has asked for APP_ASSET_UPLOADS_PER_HOUR upload URLs within the last hour.",
|
|
616
|
-
fix: "Show the user a message and retry after Retry-After seconds; never loop. For upload URLs: upload the files you already have URLs for, and ask for more after the hour."
|
|
698
|
+
surface: "module route 429 (DrobekError), Retry-After; MCP tool isError (create_asset_upload, duplicate_app); dashboard 429 (duplicate page)",
|
|
699
|
+
meaning: "A module limit was hit (per visitor, per user or per app \u2014 `details.limit` per `details.window_seconds`). From create_asset_upload: the app has asked for APP_ASSET_UPLOADS_PER_HOUR upload URLs within the last hour. From duplicate_app: the user made DUPLICATES_PER_USER_HOUR copies of gallery apps within the last hour (`limit`, `value`).",
|
|
700
|
+
fix: "Show the user a message and retry after Retry-After seconds; never loop. For upload URLs: upload the files you already have URLs for, and ask for more after the hour. For duplicate_app: tell the user to try again in an hour; do not retry."
|
|
617
701
|
},
|
|
618
702
|
{
|
|
619
703
|
code: "payload_too_large",
|
|
@@ -757,6 +841,11 @@ var LIMITS = [
|
|
|
757
841
|
default: "60",
|
|
758
842
|
meaning: "Upload URLs one app may get per hour (create_asset_upload \u2192 rate_limited)."
|
|
759
843
|
},
|
|
844
|
+
{
|
|
845
|
+
env: "DUPLICATES_PER_USER_HOUR",
|
|
846
|
+
default: "10",
|
|
847
|
+
meaning: "Copies of gallery apps one person may make per hour, duplicate_app and the dashboard together (\u2192 rate_limited)."
|
|
848
|
+
},
|
|
760
849
|
{
|
|
761
850
|
env: "tool: asset upload URL",
|
|
762
851
|
default: "30 min, single use",
|
|
@@ -805,16 +894,6 @@ var DOC_PAGES = {
|
|
|
805
894
|
licensing: "https://github.com/freema/drobek/blob/main/docs/LICENSING.md"
|
|
806
895
|
};
|
|
807
896
|
|
|
808
|
-
// packages/agent-dx/dist/plugin.js
|
|
809
|
-
var PLUGIN_REPO = "freema/drobek-plugin";
|
|
810
|
-
var PLUGIN_REPO_URL = `https://github.com/${PLUGIN_REPO}`;
|
|
811
|
-
var PLUGIN_MARKETPLACE = "drobek";
|
|
812
|
-
var PLUGIN_NAME = "drobek";
|
|
813
|
-
var PLUGIN_MARKETPLACE_ADD_COMMAND = `claude plugin marketplace add ${PLUGIN_REPO}`;
|
|
814
|
-
var PLUGIN_INSTALL_COMMAND = `claude plugin install ${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;
|
|
815
|
-
var PLUGIN_BUILD_COMMAND = `/${PLUGIN_NAME}:build-app`;
|
|
816
|
-
var PLUGIN_PORT_COMMAND = `/${PLUGIN_NAME}:port-artifact`;
|
|
817
|
-
|
|
818
897
|
// packages/agent-dx/dist/render.js
|
|
819
898
|
var AGENT_GUIDE_URL = DOC_PAGES.agent;
|
|
820
899
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freema/drobek-modules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The drobek platform module contract, registry and runtime: defineModule, the ModuleContext and the test kit (@drobek/modules/testing).",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"LICENSE"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@drobek/sdk": "npm:@freema/drobek-sdk@0.
|
|
45
|
+
"@drobek/sdk": "npm:@freema/drobek-sdk@0.5.0",
|
|
46
46
|
"@paralleldrive/cuid2": "^2.2.2",
|
|
47
47
|
"esbuild": "^0.25.0",
|
|
48
48
|
"ioredis": "^5.6.0",
|