@blamejs/blamejs-shop 0.3.56 → 0.3.58

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +11 -0
  4. package/lib/asset-manifest.json +3 -3
  5. package/lib/checkout.js +89 -10
  6. package/lib/security-middleware.js +17 -1
  7. package/lib/storefront.js +156 -51
  8. package/lib/vendor/MANIFEST.json +84 -72
  9. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  10. package/lib/vendor/blamejs/README.md +3 -3
  11. package/lib/vendor/blamejs/SECURITY.md +5 -0
  12. package/lib/vendor/blamejs/api-snapshot.json +15 -3
  13. package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
  14. package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
  15. package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
  16. package/lib/vendor/blamejs/lib/archive-read.js +215 -16
  17. package/lib/vendor/blamejs/lib/archive.js +206 -52
  18. package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
  19. package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
  20. package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
  21. package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
  22. package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
  23. package/lib/vendor/blamejs/lib/db-schema.js +120 -3
  24. package/lib/vendor/blamejs/lib/db.js +10 -3
  25. package/lib/vendor/blamejs/lib/error-page.js +93 -9
  26. package/lib/vendor/blamejs/lib/external-db.js +164 -13
  27. package/lib/vendor/blamejs/lib/guard-email.js +36 -3
  28. package/lib/vendor/blamejs/lib/http-client.js +37 -7
  29. package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
  30. package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
  31. package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
  32. package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
  33. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
  34. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
  35. package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
  36. package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
  37. package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
  38. package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
  39. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
  40. package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
  41. package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
  42. package/lib/vendor/blamejs/lib/openapi.js +225 -100
  43. package/lib/vendor/blamejs/lib/problem-details.js +15 -3
  44. package/lib/vendor/blamejs/lib/queue-local.js +148 -38
  45. package/lib/vendor/blamejs/lib/queue.js +41 -11
  46. package/lib/vendor/blamejs/lib/render.js +21 -3
  47. package/lib/vendor/blamejs/lib/router.js +13 -6
  48. package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
  49. package/lib/vendor/blamejs/lib/sse.js +7 -5
  50. package/lib/vendor/blamejs/lib/static.js +46 -17
  51. package/lib/vendor/blamejs/lib/uri-template.js +3 -1
  52. package/lib/vendor/blamejs/package.json +1 -1
  53. package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
  54. package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
  55. package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
  56. package/lib/vendor/blamejs/test/00-primitives.js +151 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
  59. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
  65. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
  66. package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
  68. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
  70. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
  74. package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
  77. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
  82. package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
  83. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
  84. package/package.json +1 -1
@@ -693,6 +693,44 @@ function run() {
693
693
  function () {});
694
694
  check("openapiServe: same-origin omits CORS", sentSO.headers["Access-Control-Allow-Origin"] == null);
695
695
 
696
+ // accessControl={ allowOrigin } echoes one validated origin + Vary: Origin
697
+ var serveAO = b.middleware.openapiServe({
698
+ document: serveDoc,
699
+ accessControl: { allowOrigin: "https://docs.example.com" },
700
+ audit: false,
701
+ });
702
+ var sentAO = { headers: null };
703
+ serveAO({ method: "GET", url: "/openapi.json", pathname: "/openapi.json", headers: {} },
704
+ { writeHead: function (s, h) { sentAO.headers = h; },
705
+ end: function () {} },
706
+ function () {});
707
+ check("openapiServe: allowOrigin echoed", sentAO.headers["Access-Control-Allow-Origin"] === "https://docs.example.com");
708
+ check("openapiServe: allowOrigin sets Vary", sentAO.headers["Vary"] === "Origin");
709
+
710
+ // allowOrigin with a default port is canonicalized (443 dropped)
711
+ var serveAOPort = b.middleware.openapiServe({
712
+ document: serveDoc,
713
+ accessControl: { allowOrigin: "https://Docs.Example.com:443" },
714
+ audit: false,
715
+ });
716
+ var sentAOPort = { headers: null };
717
+ serveAOPort({ method: "GET", url: "/openapi.json", pathname: "/openapi.json", headers: {} },
718
+ { writeHead: function (s, h) { sentAOPort.headers = h; },
719
+ end: function () {} },
720
+ function () {});
721
+ check("openapiServe: allowOrigin canonicalized", sentAOPort.headers["Access-Control-Allow-Origin"] === "https://docs.example.com");
722
+
723
+ // A bad allowOrigin throws at config time (header-injection / pathful / junk)
724
+ rejects("openapiServe: allowOrigin with path rejected",
725
+ function () { b.middleware.openapiServe({ document: serveDoc, accessControl: { allowOrigin: "https://x.example.com/docs" } }); },
726
+ /bare origin/);
727
+ rejects("openapiServe: allowOrigin CRLF rejected",
728
+ function () { b.middleware.openapiServe({ document: serveDoc, accessControl: { allowOrigin: "https://x.example.com\r\nX-Evil: 1" } }); },
729
+ /valid|bare origin/);
730
+ rejects("openapiServe: allowOrigin non-http rejected",
731
+ function () { b.middleware.openapiServe({ document: serveDoc, accessControl: { allowOrigin: "ftp://x.example.com" } }); },
732
+ /valid|origin/);
733
+
696
734
  check("openapiServe.forceRebuild is fn", typeof serve.forceRebuild === "function");
697
735
 
698
736
  // ---- bigger schema-walk coverage ----
@@ -862,12 +900,151 @@ function run() {
862
900
  });
863
901
  check("openapi.parse: dangling security", dangling.valid === false &&
864
902
  dangling.errors.join(",").indexOf("undefined scheme") !== -1);
903
+ var danglingWebhook = b.openapi.parse({
904
+ openapi: "3.2.0", info: { title: "T", version: "1.0" },
905
+ webhooks: { e: { post: { responses: { "200": { description: "ok" } }, security: [{ missing: [] }] } } },
906
+ components: { securitySchemes: {} },
907
+ });
908
+ check("openapi.parse: dangling webhook-operation security",
909
+ danglingWebhook.valid === false &&
910
+ danglingWebhook.errors.join(",").indexOf("undefined scheme") !== -1);
865
911
 
866
912
  rejects("openapi.parse: bad JSON",
867
913
  function () { b.openapi.parse("{not valid json"); }, /invalid JSON/);
868
914
  rejects("openapi.parse: bad input",
869
915
  function () { b.openapi.parse(42); }, /must be a JSON string/);
870
916
 
917
+ // ---- OpenAPI 3.2: opt-in version + webhooks + jsonSchemaDialect ----
918
+
919
+ // 3.1 stays the default emitted version unless opted in.
920
+ var apiDefault = b.openapi.create({ info: { title: "Def", version: "1.0.0" } });
921
+ check("3.2: default version still 3.1.0", apiDefault.toJson().openapi === "3.1.0");
922
+ check("3.2: no webhooks key by default", apiDefault.toJson().webhooks === undefined);
923
+ check("3.2: no jsonSchemaDialect by default", apiDefault.toJson().jsonSchemaDialect === undefined);
924
+
925
+ // Opt into 3.2 with webhooks + jsonSchemaDialect.
926
+ var api32 = b.openapi.create({
927
+ openapi: "3.2.0",
928
+ jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base",
929
+ info: { title: "Webhook API", version: "1.0.0" },
930
+ });
931
+ api32.path("get", "/health", { responses: { "200": { description: "ok" } } });
932
+ api32.webhook("newPet", "post", {
933
+ summary: "A new pet has been created",
934
+ requestBody: { content: { "application/json": { schema: { type: "object" } } } },
935
+ responses: { "200": { description: "ack" }, "410": { description: "stop" } },
936
+ });
937
+ var doc32 = api32.toJson();
938
+ check("3.2: opted-in version emitted", doc32.openapi === "3.2.0");
939
+ check("3.2: jsonSchemaDialect emitted", doc32.jsonSchemaDialect === "https://spec.openapis.org/oas/3.1/dialect/base");
940
+ check("3.2: webhooks map present", doc32.webhooks != null && doc32.webhooks.newPet != null);
941
+ check("3.2: webhook operation method", doc32.webhooks.newPet.post.summary === "A new pet has been created");
942
+ check("3.2: webhook responses retained", doc32.webhooks.newPet.post.responses["200"].description === "ack");
943
+ check("3.2: paths still emitted alongside", doc32.paths["/health"].get != null);
944
+
945
+ // YAML emits the new top-level keys.
946
+ var yaml32 = api32.toYaml();
947
+ check("3.2: YAML has webhooks", yaml32.indexOf("webhooks:") !== -1);
948
+ check("3.2: YAML has jsonSchemaDialect", yaml32.indexOf("jsonSchemaDialect:") !== -1);
949
+
950
+ // Unsupported version refused at config time.
951
+ rejects("3.2: unknown version 4.0 refused",
952
+ function () { b.openapi.create({ openapi: "4.0.0", info: { title: "x", version: "1.0.0" } }); },
953
+ /version must be one of/);
954
+
955
+ // Non-string jsonSchemaDialect refused at config time.
956
+ rejects("3.2: non-string dialect refused",
957
+ function () { b.openapi.create({ jsonSchemaDialect: 123, info: { title: "x", version: "1.0.0" } }); },
958
+ /jsonSchemaDialect/);
959
+
960
+ // Webhook duplicate operation rejected.
961
+ rejects("3.2: duplicate webhook operation",
962
+ function () {
963
+ api32.webhook("newPet", "post", { responses: { "200": { description: "ok" } } });
964
+ },
965
+ /duplicate operation/);
966
+
967
+ // Webhook bad method rejected.
968
+ rejects("3.2: webhook bad method",
969
+ function () { api32.webhook("e", "nope", { responses: { "200": { description: "ok" } } }); },
970
+ /method must be/);
971
+
972
+ // Webhook missing name rejected.
973
+ rejects("3.2: webhook missing name",
974
+ function () { api32.webhook("", "post", { responses: { "200": { description: "ok" } } }); },
975
+ /name/);
976
+
977
+ // Webhook responses still required (operation rules apply).
978
+ rejects("3.2: webhook missing responses",
979
+ function () { api32.webhook("noResp", "post", {}); },
980
+ /responses object is required/);
981
+
982
+ // Per-webhook-operation dangling security is caught.
983
+ var apiWhSec = b.openapi.create({ openapi: "3.2.0", info: { title: "WS", version: "1.0.0" } });
984
+ apiWhSec.webhook("evt", "post", {
985
+ security: [{ ghost: [] }],
986
+ responses: { "200": { description: "ok" } },
987
+ });
988
+ rejects("3.2: dangling webhook security",
989
+ function () { apiWhSec.toJson(); },
990
+ /webhook POST evt references undefined security scheme/);
991
+
992
+ // Webhook with a registered scheme passes.
993
+ var apiWhOk = b.openapi.create({ openapi: "3.2.0", info: { title: "WO", version: "1.0.0" } });
994
+ apiWhOk.security.add("bearerAuth", b.openapi.security.bearer());
995
+ apiWhOk.webhook("evt", "post", {
996
+ security: [{ bearerAuth: [] }],
997
+ responses: { "200": { description: "ok" } },
998
+ });
999
+ check("3.2: webhook with registered scheme ok", apiWhOk.toJson().webhooks.evt.post.security[0].bearerAuth.length === 0);
1000
+
1001
+ // ---- parse() accepts 3.2 + validates webhooks / jsonSchemaDialect ----
1002
+ var p32 = b.openapi.parse({
1003
+ openapi: "3.2.0",
1004
+ jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base",
1005
+ info: { title: "T", version: "1.0.0" },
1006
+ paths: { "/x": { get: { responses: { "200": { description: "ok" } } } } },
1007
+ webhooks: { newPet: { post: { responses: { "200": { description: "ack" } } } } },
1008
+ });
1009
+ check("parse: 3.2 webhook doc valid", p32.valid === true && p32.errors.length === 0);
1010
+
1011
+ // 3.1 doc still parses unchanged.
1012
+ var p31 = b.openapi.parse({ openapi: "3.1.0", info: { title: "T", version: "1.0.0" } });
1013
+ check("parse: 3.1 still valid", p31.valid === true);
1014
+
1015
+ // Unknown version 4.0 still refused.
1016
+ var p40 = b.openapi.parse({ openapi: "4.0.0", info: { title: "T", version: "1.0.0" } });
1017
+ check("parse: 4.0 invalid", p40.valid === false &&
1018
+ p40.errors.join(",").indexOf("3.2.x") !== -1);
1019
+
1020
+ // Webhook operation missing description is invalid.
1021
+ var pWhBad = b.openapi.parse({
1022
+ openapi: "3.2.0", info: { title: "T", version: "1.0.0" },
1023
+ webhooks: { e: { post: { responses: { "200": {} } } } },
1024
+ });
1025
+ check("parse: webhook missing description", pWhBad.valid === false &&
1026
+ pWhBad.errors.join(",").indexOf("webhook e") !== -1);
1027
+
1028
+ // Webhooks not an object is invalid.
1029
+ var pWhType = b.openapi.parse({
1030
+ openapi: "3.2.0", info: { title: "T", version: "1.0.0" }, webhooks: "nope",
1031
+ });
1032
+ check("parse: webhooks non-object invalid", pWhType.valid === false &&
1033
+ pWhType.errors.join(",").indexOf("webhooks") !== -1);
1034
+
1035
+ // Non-string jsonSchemaDialect is invalid.
1036
+ var pDialect = b.openapi.parse({
1037
+ openapi: "3.2.0", info: { title: "T", version: "1.0.0" }, jsonSchemaDialect: 5,
1038
+ });
1039
+ check("parse: bad jsonSchemaDialect invalid", pDialect.valid === false &&
1040
+ pDialect.errors.join(",").indexOf("jsonSchemaDialect") !== -1);
1041
+
1042
+ // Round-trip a built 3.2 doc through parse.
1043
+ var built32 = b.openapi.create({ openapi: "3.2.0", info: { title: "T", version: "1.0.0" } });
1044
+ built32.webhook("evt", "post", { responses: { "200": { description: "ok" } } });
1045
+ var roundTrip32 = b.openapi.parse(built32.toJsonString());
1046
+ check("parse: 3.2 round-trip valid", roundTrip32.valid === true);
1047
+
871
1048
  console.log("OK — openapi tests");
872
1049
  }
873
1050
 
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ /**
3
+ * queue — bring-your-own database for the local backend.
4
+ *
5
+ * The local protocol defaults to the framework DB + "_blamejs_jobs"
6
+ * table, but accepts { db, table, schema } so an operator can point the
7
+ * queue rows at their own store / table / schema. Identifiers are
8
+ * validated + quoted through b.safeSql (SQL identifier injection, CWE-89)
9
+ * and refused at config time when unsafe; sealed columns (payload,
10
+ * lastError) stay sealed regardless of which physical table the rows
11
+ * land in.
12
+ *
13
+ * Run standalone: `node test/layer-0-primitives/queue-byo-db.test.js`
14
+ * Or via smoke: `node test/smoke.js`
15
+ */
16
+
17
+ var helpers = require("../helpers");
18
+ var b = helpers.b;
19
+ var fs = helpers.fs;
20
+ var os = helpers.os;
21
+ var path = helpers.path;
22
+ var check = helpers.check;
23
+ var setupTestDb = helpers.setupTestDb;
24
+ var teardownTestDb = helpers.teardownTestDb;
25
+
26
+ function _tmp() { return fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-q-byo-")); }
27
+
28
+ // A jobs-shaped table with the same columns as _blamejs_jobs so the
29
+ // local backend can write to it when pointed there via { table }.
30
+ // payload + lastError are TEXT to hold the vault-sealed blobs.
31
+ function _createCustomJobsTable(name) {
32
+ b.db.exec(
33
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
34
+ " _id TEXT PRIMARY KEY," +
35
+ " queueName TEXT NOT NULL," +
36
+ " payload TEXT," +
37
+ " status TEXT NOT NULL," +
38
+ " enqueuedAt INTEGER NOT NULL," +
39
+ " availableAt INTEGER NOT NULL," +
40
+ " leasedAt INTEGER," +
41
+ " leaseExpiresAt INTEGER," +
42
+ " attempts INTEGER NOT NULL DEFAULT 0," +
43
+ " maxAttempts INTEGER NOT NULL DEFAULT 5," +
44
+ " lastError TEXT," +
45
+ " finishedAt INTEGER," +
46
+ " traceId TEXT," +
47
+ " classification TEXT," +
48
+ " priority INTEGER NOT NULL DEFAULT 0," +
49
+ " repeatCron TEXT," +
50
+ " repeatTimezone TEXT," +
51
+ " flowId TEXT," +
52
+ " flowChildName TEXT," +
53
+ " dependsOn TEXT" +
54
+ ")"
55
+ );
56
+ }
57
+
58
+ // ---- Default config unchanged ----
59
+
60
+ async function testDefaultConfigUsesFrameworkJobsTable() {
61
+ var tmpDir = _tmp();
62
+ await setupTestDb(tmpDir);
63
+ try {
64
+ b.queue.init({ backends: { primary: { protocol: "local" } } });
65
+ await b.queue.enqueue("default-q", { tag: "default" });
66
+
67
+ var rows = await b.db.from("_blamejs_jobs")
68
+ .where({ queueName: "default-q", status: "pending" }).all();
69
+ check("default: row lands in _blamejs_jobs", rows.length === 1);
70
+ check("default: size reads the default table",
71
+ (await b.queue.size("default-q")) === 1);
72
+ } finally {
73
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
74
+ await teardownTestDb(tmpDir);
75
+ }
76
+ }
77
+
78
+ // ---- Custom table ----
79
+
80
+ async function testCustomTableRoutesRowsThere() {
81
+ var tmpDir = _tmp();
82
+ await setupTestDb(tmpDir);
83
+ try {
84
+ _createCustomJobsTable("app_jobs");
85
+ b.queue.init({ backends: { byo: { protocol: "local", table: "app_jobs" } } });
86
+
87
+ await b.queue.enqueue("byo-q", { tag: "custom" }, { traceId: "t-1" });
88
+
89
+ var inCustom = await b.db.from("app_jobs")
90
+ .where({ queueName: "byo-q" }).all();
91
+ check("custom table: enqueued row lands in app_jobs", inCustom.length === 1);
92
+
93
+ var inDefault = await b.db.from("_blamejs_jobs")
94
+ .where({ queueName: "byo-q" }).all();
95
+ check("custom table: nothing written to _blamejs_jobs", inDefault.length === 0);
96
+
97
+ check("custom table: size reads from the custom table",
98
+ (await b.queue.size("byo-q")) === 1);
99
+ } finally {
100
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
101
+ await teardownTestDb(tmpDir);
102
+ }
103
+ }
104
+
105
+ async function testCustomTableFullLifecycleAndSealing() {
106
+ var tmpDir = _tmp();
107
+ await setupTestDb(tmpDir);
108
+ try {
109
+ _createCustomJobsTable("app_jobs");
110
+ b.queue.init({ backends: { byo: { protocol: "local", table: "app_jobs" } } });
111
+
112
+ var seen = null;
113
+ var consumer = b.queue.consume("byo-life", async function (job) {
114
+ seen = job.payload;
115
+ }, { backend: "byo", concurrency: 1, pollIntervalMs: 25, fastPollMs: 5 });
116
+
117
+ await b.queue.enqueue("byo-life", { secret: "social-9999" }, { backend: "byo" });
118
+ await helpers.waitUntil(function () { return seen !== null; }, {
119
+ timeoutMs: 4000, label: "queue byo-db: consumer leased + ran the job from the custom table",
120
+ });
121
+ check("custom table: payload round-trips through lease/unseal",
122
+ seen && seen.secret === "social-9999");
123
+
124
+ // The stored payload column is sealed — raw bytes must NOT contain the
125
+ // plaintext. Sealing keys off the _blamejs_jobs column map even though
126
+ // the physical row lives in app_jobs.
127
+ var raw = await b.db.from("app_jobs").where({ queueName: "byo-life" }).all();
128
+ check("custom table: a row exists in app_jobs", raw.length === 1);
129
+ var storedPayload = String(raw[0].payload || "");
130
+ check("custom table: stored payload is sealed (no plaintext)",
131
+ storedPayload.indexOf("social-9999") === -1 && storedPayload.length > 0);
132
+
133
+ consumer.cancel();
134
+ } finally {
135
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
136
+ await teardownTestDb(tmpDir);
137
+ }
138
+ }
139
+
140
+ // ---- Bring-your-own store handle ----
141
+
142
+ async function testCustomDbHandleReceivesCalls() {
143
+ var tmpDir = _tmp();
144
+ await setupTestDb(tmpDir);
145
+ try {
146
+ _createCustomJobsTable("handle_jobs");
147
+ // A spy handle that delegates to the framework store but records the
148
+ // SQL it was handed, so we can confirm the backend dispatches through
149
+ // the operator-supplied handle (not cluster-storage directly) and
150
+ // targets the configured table.
151
+ var sqls = [];
152
+ var spy = {
153
+ execute: function (sql, params) {
154
+ sqls.push(sql);
155
+ return b.clusterStorage.execute(sql, params);
156
+ },
157
+ executeOne: function (sql, params) {
158
+ sqls.push(sql);
159
+ return b.clusterStorage.executeOne(sql, params);
160
+ },
161
+ executeAll: function (sql, params) {
162
+ sqls.push(sql);
163
+ return b.clusterStorage.executeAll(sql, params);
164
+ },
165
+ };
166
+ b.queue.init({ backends: { byo: { protocol: "local", db: spy, table: "handle_jobs" } } });
167
+
168
+ await b.queue.enqueue("handle-q", { tag: "via-handle" }, { backend: "byo" });
169
+ await b.queue.size("handle-q", { backend: "byo" });
170
+
171
+ check("byo handle: enqueue dispatched through the supplied handle",
172
+ sqls.some(function (s) { return /INSERT INTO "handle_jobs"/.test(s); }));
173
+ check("byo handle: size dispatched through the supplied handle",
174
+ sqls.some(function (s) { return /FROM "handle_jobs"/.test(s); }));
175
+ } finally {
176
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
177
+ await teardownTestDb(tmpDir);
178
+ }
179
+ }
180
+
181
+ // ---- Schema qualifier ----
182
+
183
+ async function testSchemaQualifierIsQuoted() {
184
+ // Verifies the schema.table qualifier is composed via safeSql quoting.
185
+ // No physical schema needed — we capture the SQL through a spy handle
186
+ // and assert the quoted dotted form, which is the injection-safe shape.
187
+ var tmpDir = _tmp();
188
+ await setupTestDb(tmpDir);
189
+ try {
190
+ var sqls = [];
191
+ var spy = {
192
+ execute: function (sql) { sqls.push(sql); return Promise.resolve({ rows: [], rowCount: 0 }); },
193
+ executeOne: function (sql) { sqls.push(sql); return Promise.resolve(null); },
194
+ executeAll: function (sql) { sqls.push(sql); return Promise.resolve([]); },
195
+ };
196
+ b.queue.init({ backends: { byo: { protocol: "local", db: spy, schema: "work", table: "jobs_t" } } });
197
+ await b.queue.enqueue("sch-q", { tag: "x" }, { backend: "byo" });
198
+ check("schema qualifier: composed as quoted \"work\".\"jobs_t\"",
199
+ sqls.some(function (s) { return s.indexOf('"work"."jobs_t"') !== -1; }));
200
+ } finally {
201
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
202
+ await teardownTestDb(tmpDir);
203
+ }
204
+ }
205
+
206
+ // ---- Config-time validation (the security boundary) ----
207
+
208
+ async function testRejectsUnsafeTableAndSchemaAndHandle() {
209
+ var tmpDir = _tmp();
210
+ await setupTestDb(tmpDir);
211
+ try {
212
+ function initThrew(cfg) {
213
+ var threw = false;
214
+ try {
215
+ b.queue.init({ backends: { byo: Object.assign({ protocol: "local" }, cfg) } });
216
+ } catch (_e) { threw = true; }
217
+ try { b.queue._resetForTest(); } catch (_e2) {}
218
+ return threw;
219
+ }
220
+
221
+ check("rejects an injection-shaped table name",
222
+ initThrew({ table: "jobs; DROP TABLE users" }));
223
+ check("rejects a quote-bearing table name",
224
+ initThrew({ table: 'jobs" --' }));
225
+ check("rejects a SQL reserved word as table",
226
+ initThrew({ table: "select" }));
227
+ check("rejects a non-string table",
228
+ initThrew({ table: 123 }));
229
+ check("rejects an injection-shaped schema name",
230
+ initThrew({ table: "jobs_t", schema: "public; DROP" }));
231
+ check("rejects a non-string schema",
232
+ initThrew({ table: "jobs_t", schema: {} }));
233
+ check("rejects a db handle that is not an object",
234
+ initThrew({ db: "not-a-handle" }));
235
+ check("rejects a db handle missing required methods",
236
+ initThrew({ db: { execute: function () {} } }));
237
+
238
+ // The default (no custom table/schema/db) must still init cleanly.
239
+ var ok = true;
240
+ try {
241
+ b.queue.init({ backends: { plain: { protocol: "local" } } });
242
+ } catch (_e) { ok = false; }
243
+ check("default config still inits cleanly", ok);
244
+ } finally {
245
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
246
+ await teardownTestDb(tmpDir);
247
+ }
248
+ }
249
+
250
+ // ---- Flow routing under a custom table ----
251
+
252
+ async function testFlowSecondPassTargetsCustomTable() {
253
+ var tmpDir = _tmp();
254
+ await setupTestDb(tmpDir);
255
+ try {
256
+ _createCustomJobsTable("flow_jobs");
257
+ b.queue.init({
258
+ backends: { byo: { protocol: "local", table: "flow_jobs" } },
259
+ defaultBackend: "byo",
260
+ });
261
+
262
+ var ord = [];
263
+ var consumer = b.queue.consume("byo-flow", async function (job) {
264
+ ord.push(job.payload.tag);
265
+ }, { backend: "byo", concurrency: 4, pollIntervalMs: 25, fastPollMs: 5 });
266
+
267
+ await b.queue.enqueueFlow({
268
+ queueName: "byo-flow",
269
+ children: [
270
+ { name: "fetch", payload: { tag: "fetch" } },
271
+ { name: "transform", payload: { tag: "transform" }, dependsOn: ["fetch"] },
272
+ ],
273
+ });
274
+
275
+ // The flow children — including the dependsOn second pass — must all
276
+ // land in flow_jobs, not _blamejs_jobs.
277
+ var inCustom = await b.db.from("flow_jobs").where({ queueName: "byo-flow" }).all();
278
+ check("flow byo: both children written to the custom table", inCustom.length === 2);
279
+ var inDefault = await b.db.from("_blamejs_jobs").where({ queueName: "byo-flow" }).all();
280
+ check("flow byo: nothing leaked to _blamejs_jobs", inDefault.length === 0);
281
+
282
+ await helpers.waitUntil(function () { return ord.length === 2; }, {
283
+ timeoutMs: 5000, label: "queue byo-flow: both flow jobs processed in order",
284
+ });
285
+ check("flow byo: dependency order preserved through the custom table",
286
+ ord[0] === "fetch" && ord[1] === "transform");
287
+
288
+ consumer.cancel();
289
+ } finally {
290
+ try { await b.queue.shutdown({ timeoutMs: 500 }); } catch (_e) {}
291
+ await teardownTestDb(tmpDir);
292
+ }
293
+ }
294
+
295
+ async function run() {
296
+ await testDefaultConfigUsesFrameworkJobsTable();
297
+ await testCustomTableRoutesRowsThere();
298
+ await testCustomTableFullLifecycleAndSealing();
299
+ await testCustomDbHandleReceivesCalls();
300
+ await testSchemaQualifierIsQuoted();
301
+ await testRejectsUnsafeTableAndSchemaAndHandle();
302
+ await testFlowSecondPassTargetsCustomTable();
303
+ }
304
+
305
+ module.exports = { run: run };
306
+
307
+ if (require.main === module) {
308
+ run().then(
309
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
310
+ function (e) { process.exitCode = 1; throw e; }
311
+ );
312
+ }