@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/SECURITY.md +11 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/checkout.js +89 -10
- package/lib/security-middleware.js +17 -1
- package/lib/storefront.js +156 -51
- package/lib/vendor/MANIFEST.json +84 -72
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -3
- package/lib/vendor/blamejs/SECURITY.md +5 -0
- package/lib/vendor/blamejs/api-snapshot.json +15 -3
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
- package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
- package/lib/vendor/blamejs/lib/archive-read.js +215 -16
- package/lib/vendor/blamejs/lib/archive.js +206 -52
- package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
- package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
- package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
- package/lib/vendor/blamejs/lib/db-schema.js +120 -3
- package/lib/vendor/blamejs/lib/db.js +10 -3
- package/lib/vendor/blamejs/lib/error-page.js +93 -9
- package/lib/vendor/blamejs/lib/external-db.js +164 -13
- package/lib/vendor/blamejs/lib/guard-email.js +36 -3
- package/lib/vendor/blamejs/lib/http-client.js +37 -7
- package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
- package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
- package/lib/vendor/blamejs/lib/openapi.js +225 -100
- package/lib/vendor/blamejs/lib/problem-details.js +15 -3
- package/lib/vendor/blamejs/lib/queue-local.js +148 -38
- package/lib/vendor/blamejs/lib/queue.js +41 -11
- package/lib/vendor/blamejs/lib/render.js +21 -3
- package/lib/vendor/blamejs/lib/router.js +13 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
- package/lib/vendor/blamejs/lib/sse.js +7 -5
- package/lib/vendor/blamejs/lib/static.js +46 -17
- package/lib/vendor/blamejs/lib/uri-template.js +3 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
- package/lib/vendor/blamejs/test/00-primitives.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
- 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
|
+
}
|