@freema/drobek-modules 0.4.0 → 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-OCSLKNR2.js → chunk-B7PEIWK7.js} +110 -74
- 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 +50 -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)
|
|
@@ -223,6 +224,8 @@ __export(schema_exports, {
|
|
|
223
224
|
blobs: () => blobs,
|
|
224
225
|
compileStatusEnum: () => compileStatusEnum,
|
|
225
226
|
domains: () => domains,
|
|
227
|
+
galleryLikes: () => galleryLikes,
|
|
228
|
+
galleryOpens: () => galleryOpens,
|
|
226
229
|
membershipRoleEnum: () => membershipRoleEnum,
|
|
227
230
|
memberships: () => memberships,
|
|
228
231
|
moduleConfigs: () => moduleConfigs,
|
|
@@ -342,9 +345,16 @@ var apps = pgTable("apps", {
|
|
|
342
345
|
/** NSO-340: the gallery's one-line public description (plain text, ≤ 160 chars). */
|
|
343
346
|
galleryDescription: text("gallery_description"),
|
|
344
347
|
/** NSO-340: a super-admin hid the gallery entry (non-null = hidden, whatever the owner sets). */
|
|
345
|
-
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")
|
|
346
355
|
}, (t) => [
|
|
347
356
|
uniqueIndex("apps_slug_uq").on(t.slug),
|
|
357
|
+
index("apps_duplicated_from_idx").on(t.duplicatedFromAppId).where(sql`${t.duplicatedFromAppId} IS NOT NULL`),
|
|
348
358
|
// The public gallery page reads listed apps newest-published first (NSO-340).
|
|
349
359
|
index("apps_gallery_idx").on(t.publishedAt.desc(), t.slug.desc()).where(sql`${t.galleryListed}`),
|
|
350
360
|
index("apps_workspace_idx").on(t.workspaceId),
|
|
@@ -354,6 +364,16 @@ var apps = pgTable("apps", {
|
|
|
354
364
|
// words). A deleted app may carry its released-slug tombstone instead (0016).
|
|
355
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]+$')`)
|
|
356
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] })]);
|
|
357
377
|
var blobs = pgTable("blobs", {
|
|
358
378
|
sha256: text("sha256").primaryKey(),
|
|
359
379
|
bytes: bytea("bytes").notNull(),
|
|
@@ -697,16 +717,16 @@ async function runJournalMigrations(opts) {
|
|
|
697
717
|
const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_URL;
|
|
698
718
|
if (!databaseUrl)
|
|
699
719
|
throw new Error("DATABASE_URL is required");
|
|
700
|
-
const
|
|
720
|
+
const sql17 = postgres2(databaseUrl, { max: 1, connect_timeout: 10, onnotice: () => {
|
|
701
721
|
} });
|
|
702
722
|
try {
|
|
703
|
-
await migrate(drizzle2(
|
|
723
|
+
await migrate(drizzle2(sql17), {
|
|
704
724
|
migrationsFolder: opts.migrationsFolder,
|
|
705
725
|
migrationsTable: opts.migrationsTable,
|
|
706
726
|
migrationsSchema: "drizzle"
|
|
707
727
|
});
|
|
708
728
|
} finally {
|
|
709
|
-
await
|
|
729
|
+
await sql17.end({ timeout: 5 });
|
|
710
730
|
}
|
|
711
731
|
}
|
|
712
732
|
|
|
@@ -923,12 +943,12 @@ function looksLikeSvg(head) {
|
|
|
923
943
|
}
|
|
924
944
|
if (!doctype && matchesAt(DOCTYPE_SVG_RE, head, i)) {
|
|
925
945
|
doctype = true;
|
|
926
|
-
const
|
|
946
|
+
const gt2 = head.indexOf(">", i);
|
|
927
947
|
const bracket = head.indexOf("[", i);
|
|
928
|
-
if (
|
|
948
|
+
if (gt2 < 0)
|
|
929
949
|
return false;
|
|
930
|
-
if (bracket < 0 ||
|
|
931
|
-
i =
|
|
950
|
+
if (bracket < 0 || gt2 < bracket) {
|
|
951
|
+
i = gt2 + 1;
|
|
932
952
|
continue;
|
|
933
953
|
}
|
|
934
954
|
const close = head.indexOf("]", bracket + 1);
|
|
@@ -959,11 +979,11 @@ function readCookieValue(header, name) {
|
|
|
959
979
|
if (!header)
|
|
960
980
|
return null;
|
|
961
981
|
for (const part of header.split(";")) {
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
982
|
+
const eq22 = part.indexOf("=");
|
|
983
|
+
if (eq22 === -1)
|
|
964
984
|
continue;
|
|
965
|
-
if (part.slice(0,
|
|
966
|
-
return part.slice(
|
|
985
|
+
if (part.slice(0, eq22).trim() === name)
|
|
986
|
+
return part.slice(eq22 + 1).trim();
|
|
967
987
|
}
|
|
968
988
|
return null;
|
|
969
989
|
}
|
|
@@ -1198,6 +1218,10 @@ var AUDIT_ACTIONS = {
|
|
|
1198
1218
|
appGalleryHidden: "app.gallery_hidden",
|
|
1199
1219
|
/** NSO-340: a super-admin showed a hidden gallery entry again. */
|
|
1200
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",
|
|
1201
1225
|
/** NSO-346: a super-admin enabled an opt-in platform module for the workspace (meta: module). */
|
|
1202
1226
|
moduleWorkspaceEnable: "module.workspace_enable",
|
|
1203
1227
|
/** NSO-346: a super-admin disabled an opt-in platform module for the workspace (meta: module). */
|
|
@@ -1244,7 +1268,7 @@ async function writeAudit(entry, executor) {
|
|
|
1244
1268
|
}
|
|
1245
1269
|
|
|
1246
1270
|
// packages/audit/dist/read.server.js
|
|
1247
|
-
import { and, desc, eq, lt, or } from "drizzle-orm";
|
|
1271
|
+
import { and, desc, eq, gte, lt, or } from "drizzle-orm";
|
|
1248
1272
|
|
|
1249
1273
|
// packages/audit/dist/retention.server.js
|
|
1250
1274
|
import { lt as lt2 } from "drizzle-orm";
|
|
@@ -1269,7 +1293,7 @@ var DEFAULT_APPS_MAX_PER_WORKSPACE = 50;
|
|
|
1269
1293
|
|
|
1270
1294
|
// packages/apps/dist/versions.server.js
|
|
1271
1295
|
import { createHash as createHash3 } from "node:crypto";
|
|
1272
|
-
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";
|
|
1273
1297
|
|
|
1274
1298
|
// packages/compile/dist/compiler.js
|
|
1275
1299
|
import { mkdtempSync } from "node:fs";
|
|
@@ -1941,7 +1965,8 @@ var reportedAppColumns = {
|
|
|
1941
1965
|
name: apps.name,
|
|
1942
1966
|
workspaceId: apps.workspaceId,
|
|
1943
1967
|
workspaceSlug: workspaces.slug,
|
|
1944
|
-
lockedReason: apps.lockedReason
|
|
1968
|
+
lockedReason: apps.lockedReason,
|
|
1969
|
+
published: sql2`(${apps.publishedVersionId} IS NOT NULL)`.mapWith(Boolean)
|
|
1945
1970
|
};
|
|
1946
1971
|
var SCAN_MAX_FILE = 1024 * 1024;
|
|
1947
1972
|
var SCAN_MAX_TOTAL = 4 * 1024 * 1024;
|
|
@@ -2389,7 +2414,7 @@ var CONTROL_RE = new RegExp("[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]+", "
|
|
|
2389
2414
|
|
|
2390
2415
|
// packages/apps/dist/gallery.server.js
|
|
2391
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";
|
|
2392
|
-
var
|
|
2417
|
+
var baseColumns = {
|
|
2393
2418
|
slug: apps.slug,
|
|
2394
2419
|
name: apps.name,
|
|
2395
2420
|
description: apps.galleryDescription,
|
|
@@ -2399,9 +2424,18 @@ var itemColumns = {
|
|
|
2399
2424
|
// or module runtime dependency is needed by this public core API.
|
|
2400
2425
|
modules: sql8`(select coalesce(jsonb_agg(${moduleConfigs.module} order by ${moduleConfigs.module}), '[]'::jsonb)
|
|
2401
2426
|
from ${moduleConfigs} where ${moduleConfigs.appId} = ${apps.id}
|
|
2402
|
-
and jsonb_typeof(${moduleConfigs.config}) = 'object' and ${moduleConfigs.config} <> '{}'::jsonb)
|
|
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)`
|
|
2403
2431
|
};
|
|
2404
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";
|
|
2438
|
+
|
|
2405
2439
|
// packages/modules/dist/limits.js
|
|
2406
2440
|
var LIMITS_CACHE_TTL_SEC = 60;
|
|
2407
2441
|
var LIMITS_PROVIDER_TIMEOUT_MS = 2e3;
|
|
@@ -2699,7 +2733,7 @@ function cookiePrincipalResolver(opts) {
|
|
|
2699
2733
|
}
|
|
2700
2734
|
|
|
2701
2735
|
// packages/modules/dist/secrets.server.js
|
|
2702
|
-
import { and as
|
|
2736
|
+
import { and as and16, eq as eq16, inArray as inArray7 } from "drizzle-orm";
|
|
2703
2737
|
|
|
2704
2738
|
// packages/proxy/dist/errors.js
|
|
2705
2739
|
var ProxyError = class extends Error {
|
|
@@ -2825,7 +2859,7 @@ import https from "node:https";
|
|
|
2825
2859
|
var DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2826
2860
|
|
|
2827
2861
|
// packages/proxy/dist/upstreams.server.js
|
|
2828
|
-
import { and as
|
|
2862
|
+
import { and as and15, eq as eq15, inArray as inArray6, sql as sql11 } from "drizzle-orm";
|
|
2829
2863
|
|
|
2830
2864
|
// packages/proxy/dist/forward.server.js
|
|
2831
2865
|
import { promisify } from "node:util";
|
|
@@ -2899,11 +2933,11 @@ async function setModuleSecret(input) {
|
|
|
2899
2933
|
});
|
|
2900
2934
|
}
|
|
2901
2935
|
async function deleteModuleSecret(appId, module, name) {
|
|
2902
|
-
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 });
|
|
2903
2937
|
return rows.length > 0;
|
|
2904
2938
|
}
|
|
2905
2939
|
async function getModuleSecret(appId, module, name, env) {
|
|
2906
|
-
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);
|
|
2907
2941
|
if (!row)
|
|
2908
2942
|
return null;
|
|
2909
2943
|
return decryptSecret({ ciphertext: row.ciphertext, iv: row.iv, authTag: row.authTag, wrappedDek: row.wrappedDek, kekId: row.kekId }, env);
|
|
@@ -2911,18 +2945,18 @@ async function getModuleSecret(appId, module, name, env) {
|
|
|
2911
2945
|
async function secretsStatus(appId, module, names) {
|
|
2912
2946
|
if (names.length === 0)
|
|
2913
2947
|
return /* @__PURE__ */ new Map();
|
|
2914
|
-
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)));
|
|
2915
2949
|
return new Map(rows.map((r) => [r.name, r.updatedAt]));
|
|
2916
2950
|
}
|
|
2917
2951
|
async function secretsSet(appId, module, names) {
|
|
2918
2952
|
if (names.length === 0)
|
|
2919
2953
|
return /* @__PURE__ */ new Set();
|
|
2920
|
-
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)));
|
|
2921
2955
|
return new Set(rows.map((r) => r.name));
|
|
2922
2956
|
}
|
|
2923
2957
|
|
|
2924
2958
|
// packages/modules/dist/configs.server.js
|
|
2925
|
-
import { and as
|
|
2959
|
+
import { and as and17, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
2926
2960
|
function asObject2(v) {
|
|
2927
2961
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
2928
2962
|
}
|
|
@@ -2939,7 +2973,7 @@ function asPending(v) {
|
|
|
2939
2973
|
};
|
|
2940
2974
|
}
|
|
2941
2975
|
async function readConfigRow(appId, module, executor = getDb()) {
|
|
2942
|
-
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);
|
|
2943
2977
|
if (!row)
|
|
2944
2978
|
return { config: {}, pending: null, updatedAt: null };
|
|
2945
2979
|
return { config: asObject2(row.config), pending: asPending(row.pending), updatedAt: row.updatedAt };
|
|
@@ -2948,7 +2982,7 @@ async function readConfigRows(appId, modules) {
|
|
|
2948
2982
|
const out = /* @__PURE__ */ new Map();
|
|
2949
2983
|
if (modules.length === 0)
|
|
2950
2984
|
return out;
|
|
2951
|
-
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)));
|
|
2952
2986
|
for (const r of rows) {
|
|
2953
2987
|
out.set(r.module, { config: asObject2(r.config), pending: asPending(r.pending), updatedAt: r.updatedAt });
|
|
2954
2988
|
}
|
|
@@ -2957,7 +2991,7 @@ async function readConfigRows(appId, modules) {
|
|
|
2957
2991
|
async function withLockedConfig(appId, module, fn) {
|
|
2958
2992
|
return getDb().transaction(async (tx) => {
|
|
2959
2993
|
await tx.insert(moduleConfigs).values({ appId, module }).onConflictDoNothing();
|
|
2960
|
-
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");
|
|
2961
2995
|
const current = {
|
|
2962
2996
|
config: asObject2(row?.config),
|
|
2963
2997
|
pending: asPending(row?.pending),
|
|
@@ -2969,7 +3003,7 @@ async function withLockedConfig(appId, module, fn) {
|
|
|
2969
3003
|
set.config = next.config;
|
|
2970
3004
|
if (next.pending !== void 0)
|
|
2971
3005
|
set.pending = next.pending;
|
|
2972
|
-
await tx.update(moduleConfigs).set(set).where(
|
|
3006
|
+
await tx.update(moduleConfigs).set(set).where(and17(eq17(moduleConfigs.appId, appId), eq17(moduleConfigs.module, module)));
|
|
2973
3007
|
};
|
|
2974
3008
|
return fn(current, write, tx);
|
|
2975
3009
|
});
|
|
@@ -3430,7 +3464,7 @@ function redisMailGuard(opts) {
|
|
|
3430
3464
|
const ttl = await r.pttl(key);
|
|
3431
3465
|
return ttl > 0 ? ttl / 1e3 : fallbackSec;
|
|
3432
3466
|
};
|
|
3433
|
-
const
|
|
3467
|
+
const count3 = async (r, key, n) => {
|
|
3434
3468
|
const c = await r.incrby(key, n);
|
|
3435
3469
|
if (c === n || await r.pttl(key) < 0)
|
|
3436
3470
|
await r.pexpire(key, HOUR_MS2);
|
|
@@ -3493,7 +3527,7 @@ function redisMailGuard(opts) {
|
|
|
3493
3527
|
if (!refusal) {
|
|
3494
3528
|
const key = mailAppCounterKey(meta.app_id, cls);
|
|
3495
3529
|
const share = appShareOf(budgets, cls);
|
|
3496
|
-
const used = await
|
|
3530
|
+
const used = await count3(r, key, recipients);
|
|
3497
3531
|
if (used > share) {
|
|
3498
3532
|
if (used - recipients <= share) {
|
|
3499
3533
|
log.warn(`module e-mail: an app used its hourly share of ${cls === "sign_in" ? "sign-in codes" : "notifications"}`, {
|
|
@@ -3509,7 +3543,7 @@ function redisMailGuard(opts) {
|
|
|
3509
3543
|
if (!refusal) {
|
|
3510
3544
|
const key = mailWorkspaceCounterKey(meta.workspace_id, cls);
|
|
3511
3545
|
const share = workspaceShareOf(budgets, cls);
|
|
3512
|
-
const used = await
|
|
3546
|
+
const used = await count3(r, key, recipients);
|
|
3513
3547
|
if (used > share) {
|
|
3514
3548
|
if (used - recipients <= share) {
|
|
3515
3549
|
log.warn(`module e-mail: a workspace used its hourly share of ${cls === "sign_in" ? "sign-in codes" : "notifications"}`, {
|
|
@@ -3523,7 +3557,7 @@ function redisMailGuard(opts) {
|
|
|
3523
3557
|
}
|
|
3524
3558
|
}
|
|
3525
3559
|
if (!refusal) {
|
|
3526
|
-
const used = await
|
|
3560
|
+
const used = await count3(r, MAIL_COUNTER_KEYS[cls], recipients);
|
|
3527
3561
|
if (used > budgets[cls]) {
|
|
3528
3562
|
await r.set(MAIL_PAUSE_KEYS[cls], "1", "PX", pauseMs);
|
|
3529
3563
|
await r.set(MAIL_COUNTER_KEYS[cls], "0", "PX", pauseMs);
|
|
@@ -4132,43 +4166,43 @@ function readModulesLock(modulesDir) {
|
|
|
4132
4166
|
// packages/modules/dist/migration-lint.js
|
|
4133
4167
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4134
4168
|
import { join as join7 } from "node:path";
|
|
4135
|
-
function blank(
|
|
4136
|
-
const out =
|
|
4169
|
+
function blank(sql17) {
|
|
4170
|
+
const out = sql17.split("");
|
|
4137
4171
|
const wipe = (from, to) => {
|
|
4138
4172
|
for (let k = from; k < to; k++)
|
|
4139
4173
|
if (out[k] !== "\n")
|
|
4140
4174
|
out[k] = " ";
|
|
4141
4175
|
};
|
|
4142
4176
|
let i = 0;
|
|
4143
|
-
while (i <
|
|
4144
|
-
const c =
|
|
4145
|
-
if (c === "-" &&
|
|
4146
|
-
const end =
|
|
4147
|
-
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;
|
|
4148
4182
|
wipe(i, stop);
|
|
4149
4183
|
i = stop;
|
|
4150
|
-
} else if (c === "/" &&
|
|
4151
|
-
const end =
|
|
4152
|
-
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;
|
|
4153
4187
|
wipe(i, stop);
|
|
4154
4188
|
i = stop;
|
|
4155
4189
|
} else if (c === "'") {
|
|
4156
4190
|
let j = i + 1;
|
|
4157
|
-
while (j <
|
|
4158
|
-
if (
|
|
4191
|
+
while (j < sql17.length) {
|
|
4192
|
+
if (sql17[j] === "'" && sql17[j + 1] === "'")
|
|
4159
4193
|
j += 2;
|
|
4160
|
-
else if (
|
|
4194
|
+
else if (sql17[j] === "'")
|
|
4161
4195
|
break;
|
|
4162
4196
|
else
|
|
4163
4197
|
j++;
|
|
4164
4198
|
}
|
|
4165
|
-
wipe(i, Math.min(j + 1,
|
|
4199
|
+
wipe(i, Math.min(j + 1, sql17.length));
|
|
4166
4200
|
i = j + 1;
|
|
4167
4201
|
} else if (c === '"') {
|
|
4168
|
-
const end =
|
|
4169
|
-
i = end === -1 ?
|
|
4202
|
+
const end = sql17.indexOf('"', i + 1);
|
|
4203
|
+
i = end === -1 ? sql17.length : end + 1;
|
|
4170
4204
|
} else if (c === "$") {
|
|
4171
|
-
const tag = /^\$[A-Za-z_]*\$/.exec(
|
|
4205
|
+
const tag = /^\$[A-Za-z_]*\$/.exec(sql17.slice(i));
|
|
4172
4206
|
if (tag) {
|
|
4173
4207
|
wipe(i, i + tag[0].length);
|
|
4174
4208
|
i += tag[0].length;
|
|
@@ -4188,8 +4222,8 @@ function splitName(raw) {
|
|
|
4188
4222
|
const unq = (p) => p.startsWith('"') ? p.slice(1, -1).replace(/""/g, '"') : p.toLowerCase();
|
|
4189
4223
|
return parts.length === 2 ? { schema: unq(parts[0]), name: unq(parts[1]) } : { schema: null, name: unq(parts[0] ?? "") };
|
|
4190
4224
|
}
|
|
4191
|
-
function lintMigrationSql(name,
|
|
4192
|
-
const text2 = blank(
|
|
4225
|
+
function lintMigrationSql(name, sql17, file = "migration.sql") {
|
|
4226
|
+
const text2 = blank(sql17);
|
|
4193
4227
|
const prefix = `mod_${name}`;
|
|
4194
4228
|
const own = (raw) => {
|
|
4195
4229
|
const { schema, name: n } = splitName(raw);
|
|
@@ -5008,20 +5042,20 @@ var LOGS_RETENTION_DAYS = 30;
|
|
|
5008
5042
|
import { createHash as createHash8 } from "node:crypto";
|
|
5009
5043
|
|
|
5010
5044
|
// packages/insights/dist/resolve.server.js
|
|
5011
|
-
import { and as
|
|
5045
|
+
import { and as and18, eq as eq18, isNull as isNull7 } from "drizzle-orm";
|
|
5012
5046
|
|
|
5013
5047
|
// packages/insights/dist/signals.server.js
|
|
5014
|
-
import { sql as
|
|
5048
|
+
import { sql as sql12 } from "drizzle-orm";
|
|
5015
5049
|
var SIGNAL_TTL_SEC = (LOGS_RETENTION_DAYS + 1) * 24 * 60 * 60;
|
|
5016
|
-
function
|
|
5050
|
+
function utcDay2(d = /* @__PURE__ */ new Date()) {
|
|
5017
5051
|
return d.toISOString().slice(0, 10);
|
|
5018
5052
|
}
|
|
5019
5053
|
|
|
5020
5054
|
// packages/insights/dist/beacon.server.js
|
|
5021
|
-
import { sql as
|
|
5055
|
+
import { sql as sql13 } from "drizzle-orm";
|
|
5022
5056
|
|
|
5023
5057
|
// packages/insights/dist/query.server.js
|
|
5024
|
-
import { and as
|
|
5058
|
+
import { and as and19, desc as desc7, eq as eq19, gte as gte4 } from "drizzle-orm";
|
|
5025
5059
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5026
5060
|
|
|
5027
5061
|
// packages/insights/dist/logs.js
|
|
@@ -5037,10 +5071,10 @@ function statusClass(status) {
|
|
|
5037
5071
|
}
|
|
5038
5072
|
|
|
5039
5073
|
// packages/insights/dist/logs.server.js
|
|
5040
|
-
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";
|
|
5041
5075
|
|
|
5042
5076
|
// packages/insights/dist/module-stats.server.js
|
|
5043
|
-
import { sql as
|
|
5077
|
+
import { sql as sql14 } from "drizzle-orm";
|
|
5044
5078
|
var MODULE_STATS_FLUSH_SEC = 60;
|
|
5045
5079
|
var MODULE_STATS_TTL_SEC = (LOGS_RETENTION_DAYS + 1) * 24 * 60 * 60;
|
|
5046
5080
|
var sharedRedis = () => getRedis();
|
|
@@ -5053,7 +5087,7 @@ function flushMarkerKey(appId, day) {
|
|
|
5053
5087
|
async function recordModuleRequest(appId, module, status, opts = {}) {
|
|
5054
5088
|
try {
|
|
5055
5089
|
const r = (opts.redis ?? sharedRedis)();
|
|
5056
|
-
const day =
|
|
5090
|
+
const day = utcDay2(opts.now ?? /* @__PURE__ */ new Date());
|
|
5057
5091
|
const key = moduleCountersKey(appId, day);
|
|
5058
5092
|
if (await r.hincrby(key, `${module}:${statusClass(status)}`, 1) === 1)
|
|
5059
5093
|
await r.expire(key, MODULE_STATS_TTL_SEC);
|
|
@@ -5070,10 +5104,10 @@ function moduleStatRows(appId, day, counters) {
|
|
|
5070
5104
|
const at = field.lastIndexOf(":");
|
|
5071
5105
|
const module = field.slice(0, at);
|
|
5072
5106
|
const cls = field.slice(at + 1);
|
|
5073
|
-
const
|
|
5074
|
-
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)
|
|
5075
5109
|
continue;
|
|
5076
|
-
rows.push({ appId, module, statusClass: cls, day, count:
|
|
5110
|
+
rows.push({ appId, module, statusClass: cls, day, count: count3 });
|
|
5077
5111
|
}
|
|
5078
5112
|
return rows;
|
|
5079
5113
|
}
|
|
@@ -5082,7 +5116,7 @@ async function upsertModuleStats(rows) {
|
|
|
5082
5116
|
return;
|
|
5083
5117
|
await getDb().insert(moduleRequestStats).values(rows).onConflictDoUpdate({
|
|
5084
5118
|
target: [moduleRequestStats.appId, moduleRequestStats.module, moduleRequestStats.statusClass, moduleRequestStats.day],
|
|
5085
|
-
set: { count:
|
|
5119
|
+
set: { count: sql14`greatest(${moduleRequestStats.count}, excluded.count)` }
|
|
5086
5120
|
});
|
|
5087
5121
|
}
|
|
5088
5122
|
async function flushModuleRequests(appId, day, opts = {}) {
|
|
@@ -5097,12 +5131,12 @@ async function flushModuleRequests(appId, day, opts = {}) {
|
|
|
5097
5131
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
5098
5132
|
|
|
5099
5133
|
// packages/insights/dist/prune.server.js
|
|
5100
|
-
import { lt as lt6, sql as
|
|
5134
|
+
import { lt as lt6, sql as sql16 } from "drizzle-orm";
|
|
5101
5135
|
var DEFAULT_LOGS_PRUNE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
5102
5136
|
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
5103
5137
|
|
|
5104
5138
|
// packages/modules/dist/runtime.js
|
|
5105
|
-
import { and as
|
|
5139
|
+
import { and as and21, eq as eq21, inArray as inArray9 } from "drizzle-orm";
|
|
5106
5140
|
import { z } from "zod";
|
|
5107
5141
|
function redisRateLimiter(redis) {
|
|
5108
5142
|
return async (key, max, windowMs) => {
|
|
@@ -5149,7 +5183,7 @@ function smtpEmailTransport(log, env = process.env) {
|
|
|
5149
5183
|
};
|
|
5150
5184
|
}
|
|
5151
5185
|
async function appOwnerEmails(db2, workspaceId) {
|
|
5152
|
-
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"])));
|
|
5153
5187
|
return rows.map((r) => r.email);
|
|
5154
5188
|
}
|
|
5155
5189
|
function uniqueKeyOf(value, unique) {
|
|
@@ -5306,7 +5340,7 @@ var ModuleRuntime = class {
|
|
|
5306
5340
|
return out;
|
|
5307
5341
|
const plan = await this.deps.limits.fromPlan?.(workspaceId) ?? null;
|
|
5308
5342
|
const env = this.deps.limits.defaults();
|
|
5309
|
-
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)))) : [];
|
|
5310
5344
|
const byModule = new Map(rows.map((r) => [r.module, { enabledBy: r.enabledBy, enabledAt: r.enabledAt }]));
|
|
5311
5345
|
for (const m of modules) {
|
|
5312
5346
|
const key = moduleEnabledLimitName(m.name);
|
|
@@ -5386,7 +5420,7 @@ var ModuleRuntime = class {
|
|
|
5386
5420
|
});
|
|
5387
5421
|
}
|
|
5388
5422
|
return this.deps.db().transaction(async (tx) => {
|
|
5389
|
-
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;
|
|
5390
5424
|
if (changed) {
|
|
5391
5425
|
await writeAudit({
|
|
5392
5426
|
workspaceId: input.workspaceId,
|
|
@@ -5459,7 +5493,7 @@ var ModuleRuntime = class {
|
|
|
5459
5493
|
return null;
|
|
5460
5494
|
const deps = this.deps;
|
|
5461
5495
|
const db2 = deps.db();
|
|
5462
|
-
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);
|
|
5463
5497
|
if (!row || row.deletedAt || row.lockedReason)
|
|
5464
5498
|
return null;
|
|
5465
5499
|
const app = { id: row.id, slug: row.slug, workspaceId: row.workspaceId };
|
|
@@ -5931,7 +5965,7 @@ var ModuleRuntime = class {
|
|
|
5931
5965
|
async workspaceOf(appId) {
|
|
5932
5966
|
if (this.optInModules().length === 0)
|
|
5933
5967
|
return "";
|
|
5934
|
-
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);
|
|
5935
5969
|
return row?.workspaceId ?? "";
|
|
5936
5970
|
}
|
|
5937
5971
|
requireModule(name) {
|
|
@@ -6062,7 +6096,7 @@ var ModuleRuntime = class {
|
|
|
6062
6096
|
const waiting = await this.pendingSummary(app.id);
|
|
6063
6097
|
if (waiting.length === 0)
|
|
6064
6098
|
return;
|
|
6065
|
-
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);
|
|
6066
6100
|
const mail = pendingMail({
|
|
6067
6101
|
appName: row?.name ?? app.slug,
|
|
6068
6102
|
modules: waiting.map((w) => ({ ...w, confirmUrl: confirmUrl(deps.env, app.workspaceSlug, app.slug, w.module) }))
|
|
@@ -6256,11 +6290,11 @@ var ModuleRuntime = class {
|
|
|
6256
6290
|
}
|
|
6257
6291
|
/** get_logs `requests`: one response of a matched route of an active module (fire-and-forget). */
|
|
6258
6292
|
countRequest(appId, module, status) {
|
|
6259
|
-
const
|
|
6260
|
-
if (!
|
|
6293
|
+
const count3 = this.deps.requestStats;
|
|
6294
|
+
if (!count3)
|
|
6261
6295
|
return;
|
|
6262
6296
|
try {
|
|
6263
|
-
void Promise.resolve(
|
|
6297
|
+
void Promise.resolve(count3(appId, module, status)).catch(() => void 0);
|
|
6264
6298
|
} catch {
|
|
6265
6299
|
}
|
|
6266
6300
|
}
|
|
@@ -6504,6 +6538,8 @@ export {
|
|
|
6504
6538
|
decideAccess,
|
|
6505
6539
|
workspaces,
|
|
6506
6540
|
apps,
|
|
6541
|
+
moduleConfigs,
|
|
6542
|
+
getDb,
|
|
6507
6543
|
noopLogger,
|
|
6508
6544
|
perIpLimitKey,
|
|
6509
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
|
{
|
|
@@ -404,6 +424,16 @@ var TOOL_DOCS = [
|
|
|
404
424
|
];
|
|
405
425
|
var TOOL_NAMES = TOOL_DOCS.map((t) => t.name);
|
|
406
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
|
+
|
|
407
437
|
// packages/agent-dx/dist/errors-catalogue.js
|
|
408
438
|
var ERROR_CATALOGUE = [
|
|
409
439
|
// ── MCP tools (isError: true, body { code, message, hint }) ───────────────
|
|
@@ -505,10 +535,16 @@ var ERROR_CATALOGUE = [
|
|
|
505
535
|
},
|
|
506
536
|
{
|
|
507
537
|
code: "gallery_disabled",
|
|
508
|
-
surface: "MCP tool isError (set_gallery_listing)",
|
|
538
|
+
surface: "MCP tool isError (set_gallery_listing, duplicate_app)",
|
|
509
539
|
meaning: "This server runs no public gallery (its operator left GALLERY_ENABLED off). Nothing changed.",
|
|
510
540
|
fix: "Tell the user this server has no public gallery; do not retry."
|
|
511
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
|
+
},
|
|
512
548
|
{
|
|
513
549
|
code: "asset_too_large",
|
|
514
550
|
surface: "MCP tool isError (create_asset_upload); upload URL 413",
|
|
@@ -659,9 +695,9 @@ var ERROR_CATALOGUE = [
|
|
|
659
695
|
},
|
|
660
696
|
{
|
|
661
697
|
code: "rate_limited",
|
|
662
|
-
surface: "module route 429 (DrobekError), Retry-After; MCP tool isError (create_asset_upload)",
|
|
663
|
-
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.",
|
|
664
|
-
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."
|
|
665
701
|
},
|
|
666
702
|
{
|
|
667
703
|
code: "payload_too_large",
|
|
@@ -805,6 +841,11 @@ var LIMITS = [
|
|
|
805
841
|
default: "60",
|
|
806
842
|
meaning: "Upload URLs one app may get per hour (create_asset_upload \u2192 rate_limited)."
|
|
807
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
|
+
},
|
|
808
849
|
{
|
|
809
850
|
env: "tool: asset upload URL",
|
|
810
851
|
default: "30 min, single use",
|
|
@@ -853,16 +894,6 @@ var DOC_PAGES = {
|
|
|
853
894
|
licensing: "https://github.com/freema/drobek/blob/main/docs/LICENSING.md"
|
|
854
895
|
};
|
|
855
896
|
|
|
856
|
-
// packages/agent-dx/dist/plugin.js
|
|
857
|
-
var PLUGIN_REPO = "freema/drobek-plugin";
|
|
858
|
-
var PLUGIN_REPO_URL = `https://github.com/${PLUGIN_REPO}`;
|
|
859
|
-
var PLUGIN_MARKETPLACE = "drobek";
|
|
860
|
-
var PLUGIN_NAME = "drobek";
|
|
861
|
-
var PLUGIN_MARKETPLACE_ADD_COMMAND = `claude plugin marketplace add ${PLUGIN_REPO}`;
|
|
862
|
-
var PLUGIN_INSTALL_COMMAND = `claude plugin install ${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;
|
|
863
|
-
var PLUGIN_BUILD_COMMAND = `/${PLUGIN_NAME}:build-app`;
|
|
864
|
-
var PLUGIN_PORT_COMMAND = `/${PLUGIN_NAME}:port-artifact`;
|
|
865
|
-
|
|
866
897
|
// packages/agent-dx/dist/render.js
|
|
867
898
|
var AGENT_GUIDE_URL = DOC_PAGES.agent;
|
|
868
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",
|