@freema/drobek-modules 0.3.3 → 0.4.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.
@@ -95,6 +95,8 @@ var CORE_ERROR_CODES = [
95
95
  "domain_taken",
96
96
  "domain_not_verified",
97
97
  "dns_unavailable",
98
+ // proxy upstreams (MCP upstream tools)
99
+ "upstream_already_registered",
98
100
  // compile.errors[]
99
101
  "build_error",
100
102
  "unresolved_import",
@@ -952,6 +954,20 @@ function hasControlBytes(chunk) {
952
954
  return false;
953
955
  }
954
956
 
957
+ // packages/core/dist/cookies.js
958
+ function readCookieValue(header, name) {
959
+ if (!header)
960
+ return null;
961
+ for (const part of header.split(";")) {
962
+ const eq20 = part.indexOf("=");
963
+ if (eq20 === -1)
964
+ continue;
965
+ if (part.slice(0, eq20).trim() === name)
966
+ return part.slice(eq20 + 1).trim();
967
+ }
968
+ return null;
969
+ }
970
+
955
971
  // packages/core/dist/redis.js
956
972
  import { Redis } from "ioredis";
957
973
  var client = null;
@@ -2373,7 +2389,18 @@ var CONTROL_RE = new RegExp("[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]+", "
2373
2389
 
2374
2390
  // packages/apps/dist/gallery.server.js
2375
2391
  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 itemColumns = { slug: apps.slug, name: apps.name, description: apps.galleryDescription, publishedAt: apps.publishedAt };
2392
+ var itemColumns = {
2393
+ slug: apps.slug,
2394
+ name: apps.name,
2395
+ description: apps.galleryDescription,
2396
+ publishedAt: apps.publishedAt,
2397
+ // Names only, from the current saved configuration. A correlated aggregate
2398
+ // keeps pagination/counts intact and avoids a query per card. No extension
2399
+ // or module runtime dependency is needed by this public core API.
2400
+ modules: sql8`(select coalesce(jsonb_agg(${moduleConfigs.module} order by ${moduleConfigs.module}), '[]'::jsonb)
2401
+ from ${moduleConfigs} where ${moduleConfigs.appId} = ${apps.id}
2402
+ and jsonb_typeof(${moduleConfigs.config}) = 'object' and ${moduleConfigs.config} <> '{}'::jsonb)`
2403
+ };
2377
2404
 
2378
2405
  // packages/modules/dist/limits.js
2379
2406
  var LIMITS_CACHE_TTL_SEC = 60;
@@ -2570,20 +2597,8 @@ function endUserEpochKey(appId) {
2570
2597
  }
2571
2598
  var SESSION_PROVIDER_RE = /^[a-z][a-z0-9]{1,15}$/;
2572
2599
  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
2600
  function readEndUserToken(cookieHeader, secure) {
2586
- const token = readCookie(cookieHeader, endUserCookieName(secure));
2601
+ const token = readCookieValue(cookieHeader, endUserCookieName(secure));
2587
2602
  return token && END_USER_TOKEN_RE.test(token) ? token : null;
2588
2603
  }
2589
2604
  function parseEndUserSession(raw) {
package/dist/index.js CHANGED
@@ -132,7 +132,7 @@ import {
132
132
  stableJson,
133
133
  streamMultipartFile,
134
134
  submissionsAuthorityOf
135
- } from "./chunk-WHKPW5MP.js";
135
+ } from "./chunk-OCSLKNR2.js";
136
136
 
137
137
  // packages/modules/dist/index.js
138
138
  import { z as z2 } from "zod";
package/dist/testing.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  runRoute,
24
24
  sanitizeSubject,
25
25
  workspaces
26
- } from "./chunk-WHKPW5MP.js";
26
+ } from "./chunk-OCSLKNR2.js";
27
27
 
28
28
  // packages/modules/dist/testing.js
29
29
  import { existsSync } from "node:fs";
@@ -340,6 +340,48 @@ var TOOL_DOCS = [
340
340
  returns: "{ removed:host, was_verified, was_primary, note }",
341
341
  example: { app_id: "k3v9x0\u2026", host: "shop.example.com", user_confirmed: true }
342
342
  },
343
+ {
344
+ name: "list_upstreams",
345
+ title: "List a workspace's proxy upstreams",
346
+ scope: "read (workspace-admin role in the workspace)",
347
+ 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.",
348
+ annotations: READ_ONLY,
349
+ fields: [{ name: "workspace", type: "string", required: true, description: "The workspace slug." }],
350
+ 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 }',
351
+ example: { workspace: "acme-crew" }
352
+ },
353
+ {
354
+ name: "register_upstream",
355
+ title: "Register a proxy upstream",
356
+ scope: "write (workspace-admin role in the workspace)",
357
+ 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.',
358
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
359
+ fields: [
360
+ { name: "workspace", type: "string", required: true, description: "The workspace slug." },
361
+ { name: "name", type: "string", required: true, description: "The name apps call it by, e.g. pokeapi." },
362
+ { name: "base_url", type: "string", required: true, description: "The base URL, e.g. https://pokeapi.co." },
363
+ { name: "allowed_methods", type: "string[]", required: true, description: 'e.g. ["GET"].' },
364
+ { name: "allowed_path_prefixes", type: "string[]", required: true, description: 'e.g. ["/api/v2/"].' },
365
+ { name: "auth_type", type: '"none" | "bearer" | "header"', required: true, description: "none registers now; bearer / header answer secret_url." },
366
+ { name: "auth_header_name", type: "string (header only)", required: false, description: "The header that carries the key, e.g. X-Api-Key." }
367
+ ],
368
+ 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",
369
+ example: { workspace: "acme-crew", name: "pokeapi", base_url: "https://pokeapi.co", allowed_methods: ["GET"], allowed_path_prefixes: ["/api/v2/"], auth_type: "none" }
370
+ },
371
+ {
372
+ name: "remove_upstream",
373
+ title: "Remove a proxy upstream",
374
+ scope: "write (workspace-admin role in the workspace)",
375
+ 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.",
376
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
377
+ fields: [
378
+ { name: "workspace", type: "string", required: true, description: "The workspace slug." },
379
+ { name: "name", type: "string", required: true, description: "A registered upstream (list_upstreams lists them)." },
380
+ { name: "user_confirmed", type: "boolean", required: false, description: "true ONLY after the user explicitly said yes to removing it." }
381
+ ],
382
+ returns: "{ removed:name, apps:[slug], note }",
383
+ example: { workspace: "acme-crew", name: "pokeapi", user_confirmed: true }
384
+ },
343
385
  {
344
386
  name: "set_workspace_publishing",
345
387
  title: "Set a workspace's publishing",
@@ -545,6 +587,12 @@ var ERROR_CATALOGUE = [
545
587
  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
588
  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
589
  },
590
+ {
591
+ code: "upstream_already_registered",
592
+ surface: "MCP tool isError (register_upstream)",
593
+ meaning: "The workspace already has an upstream with this name. Nothing changed.",
594
+ 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."
595
+ },
548
596
  {
549
597
  code: "internal_error",
550
598
  surface: "MCP tool isError",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freema/drobek-modules",
3
- "version": "0.3.3",
3
+ "version": "0.4.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.3.3",
45
+ "@drobek/sdk": "npm:@freema/drobek-sdk@0.4.0",
46
46
  "@paralleldrive/cuid2": "^2.2.2",
47
47
  "esbuild": "^0.25.0",
48
48
  "ioredis": "^5.6.0",