@blamejs/core 0.15.1 → 0.15.3
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/lib/db-schema.js +19 -4
- package/lib/network-proxy.js +24 -1
- package/lib/object-store/gcs.js +4 -1
- package/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/object-store/sigv4.js +28 -6
- package/lib/sql.js +53 -12
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.3 (2026-06-12) — **DDL hardening in b.sql, schema-confined column introspection on Postgres and MySQL, and a classical-downgrade audit on proxy-tunneled TLS.** This release hardens the data layer and closes a transport audit gap. The b.sql builder refuses an unrecognised column type that carries a statement terminator, quote, or comment marker - the one position in an otherwise quote-by-construction DDL builder where a verbatim string reached the emitted statement - and routes the finished CREATE TABLE through the same single-statement gate every other verb uses. The schema reconciler's column introspection is now confined to the schema or database the bare-named CREATE TABLE actually writes into (current_schema() on Postgres, DATABASE() on MySQL), so a same-named table in another schema no longer pollutes the column set, silently skipping an ADD COLUMN or fabricating false schema drift that refuses a regulated-posture boot. Two further builder gaps are fixed: a column-level primary key combined with a composite primaryKey now fails at build time with a clear error instead of producing invalid DDL, and a MySQL upsert read-back keyed by a cast or a server-evaluated function now renders the cast (or refuses the function) instead of binding an internal wrapper. Finally, an HTTPS request sent through a configured proxy now emits the tls.classical_downgrade audit when the handshake falls back to a classical group, the same as a direct connection. **Fixed:** *Schema reconciliation reads columns from the right schema on Postgres and MySQL* — The reconciler's column introspection queried information_schema with no schema filter, so on a Postgres instance or MySQL server hosting more than one schema/database with a same-named table, the live column set was the union across schemas. That could silently skip an ADD COLUMN the table needed, or report false drift that refuses a boot under a pinned regulated posture. Introspection is now confined to current_schema() (Postgres) / DATABASE() (MySQL) - the schema the bare-named CREATE TABLE lands in. SQLite (PRAGMA, per-file) is unchanged. · *createTable rejects a contradictory primary-key declaration at build time* — Declaring both a column-level primary key (primaryKey / autoIncrement / serial) and a composite opts.primaryKey emitted two PRIMARY KEY clauses, which every dialect rejects at the driver mid-migration. The builder now refuses the contradiction at build time with a clear error; a single column PK, or a composite primaryKey with no column-level PK, is unaffected. · *MySQL upsert read-back resolves a cast or function conflict key instead of binding a wrapper* — On MySQL, an upsert whose conflict key was a b.sql.cast(...) or b.sql.fn(...) built a read-back SELECT that bound the wrapper object, so the read-back matched no rows. A cast conflict key now renders as CAST(? AS type) binding the inner value; a server-evaluated function conflict key (which has no stable read-back identity) is refused with a clear error. Plain scalar conflict keys are unchanged. · *Proxy-tunneled TLS emits the classical-downgrade audit* — An HTTPS upstream reached through a configured proxy performed its TLS handshake without emitting the tls.classical_downgrade audit on a classical-group fallback, leaving the post-quantum-readiness inventory incomplete for proxied requests. Both the upstream handshake and the proxy-leg handshake now emit the audit on a classical fallback, matching the direct connection path. The handshake itself is unchanged (still hybrid-preferred TLSv1.3). **Security:** *b.sql refuses an injection-bearing verbatim column type and gates every CREATE TABLE* — An unrecognised column type passed to b.sql.createTable / alterTable was emitted into the DDL verbatim - the single raw-emission position in a builder that otherwise quotes every identifier and guards every constraint fragment. A type such as "text); DROP TABLE secrets; --" could therefore smuggle a stacked statement. The builder now refuses, at build time, a verbatim type carrying a statement terminator or comment marker, and routes the finished CREATE TABLE / ALTER TABLE statement through the same single-statement / NUL / unterminated-quote / unbalanced-paren gate every SELECT / INSERT / UPDATE / DELETE / UPSERT already used - so an unbalanced quote is caught there. Legitimate types are unaffected: VARCHAR(255), NUMERIC(10,2), DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, and MySQL ENUM('a','b') / SET(...) (which need balanced quotes) all still pass.
|
|
12
|
+
|
|
13
|
+
- v0.15.2 (2026-06-12) — **Object keys with a space, +, &, or other reserved characters now sign correctly against S3-compatible stores and Google Cloud Storage.** The SigV4 request signer (and the Google Cloud Storage V4 signer that shares it) URI-encoded the object-key path a second time when building the canonical request it signs, while the request on the wire carried the path encoded once. Amazon S3 — and every S3-compatible store such as MinIO, Cloudflare R2, Wasabi, and Backblaze B2, plus GCS — signs the canonical path encoded exactly once, so any object key containing a space, a +, an &, parentheses, or a non-ASCII character was signed over a path the server never received and the request was rejected with HTTP 403 SignatureDoesNotMatch. Keys built only from unreserved characters were unaffected, which is why the regression went unnoticed. This release makes the canonical-path encoding match the service: S3 and GCS encode the path once, while the AWS services that genuinely require a second encoding (SQS, CloudWatch Logs, SNS) keep it. Object reads, writes, deletes, listings, presigned URLs, and backup or restore through the object-store adapter now succeed for keys with reserved characters. Separately, the bucket-operations key encoder now uses the AWS reserved-character set, so a key containing !, *, ', or ( is escaped to match the bytes the store signs over. **Fixed:** *SigV4 and GCS V4 sign object-key paths with the single encoding S3 and GCS expect* — A request to read, write, delete, list, or presign an object whose key contained a space, +, &, (, ), or a non-ASCII character failed with 403 SignatureDoesNotMatch against S3, every S3-compatible store, and Google Cloud Storage, because the canonical request double-encoded the path the wire carried once. The signer is now service-aware: S3 and GCS sign the path encoded once (matching the wire and the store's own canonicalization), while SQS, CloudWatch Logs, and SNS keep the second encoding the AWS spec requires for those services. No configuration change or migration is needed — object operations and presigned URLs for keys with reserved characters simply start working. Object keys made only of unreserved characters are byte-for-byte unchanged. · *Bucket-operations key encoder escapes the AWS reserved set* — The bucket-level object operations encoded key path segments with a generic URI encoder that left !, *, ', and ( unescaped. Those now route through the same AWS encoder the read and write paths use, so a key containing one of those characters is escaped consistently and signs correctly.
|
|
14
|
+
|
|
11
15
|
- v0.15.1 (2026-06-11) — **Sealed-column lookups find rows written before the v0.15.0 hash change, and API-key secrets re-hash to the active algorithm on verify.** v0.15.0 changed the default derived hash — the blind index a sealed column is looked up by — from an unkeyed salted hash to a keyed MAC, and promised a transparent migration via a dual read. But no lookup path actually performed the dual read, so on a deployment that already held data, a lookup by a sealed column (a session's user id, an API key's owner, an audit actor or resource, a consent or data-subject id, a mail thread) computed only the new keyed digest and missed every row written before the upgrade. This release wires the dual read into every lookup: a sealed-column equality now matches both the active keyed digest and the legacy salted digest, so pre-upgrade rows are found again. That restores two correctness guarantees the gap had quietly broken — revoking all of a user's sessions no longer skips sessions created before the upgrade, and a subject erasure no longer leaves pre-upgrade rows behind. Separately, the framework's API-key store now re-hashes a stored secret to the active hash algorithm on the next successful verify, the transparent rotation the credential-hash primitive documented but the store had never performed. **Fixed:** *Sealed-column lookups match rows written before the v0.15.0 keyed-MAC change* — After v0.15.0 flipped the default derived-hash mode to a keyed MAC, a lookup by a sealed column computed only the new keyed digest, so rows written under the previous salted-hash default were no longer found — a silent index miss on every existing deployment. Every framework lookup path now dual-reads: `b.db.from(...).where(sealedField, value)` and the framework's own api-key, session, audit, consent, data-subject, and mail-thread lookups match the column against both the active keyed digest and the legacy salted digest (`b.db.hashCandidatesFor` exposes the candidate list for operator code). No migration or operator action is required; rows re-hash to the keyed form on read over time and the candidate set collapses back to a single value. Two correctness consequences are restored: revoking all of a user's sessions now also revokes sessions created before the upgrade, and a data-subject erasure now also deletes (and crypto-shreds) the subject's pre-upgrade rows. · *API-key secret hashes upgrade to the active algorithm on verify* — The framework's API-key store now re-hashes a stored secret to the configured hash algorithm on the next successful verify (leader-gated, best-effort, primary-match only), emitting an `apikey.secret_rehash` audit and observability event. This is the transparent rotation `b.credentialHash` documents — a key stored under an older algorithm or parameter set silently moves to the current one as it is used, with no change to the verify result or the returned record.
|
|
12
16
|
|
|
13
17
|
- v0.15.0 (2026-06-08) — **A chainable SQL builder, MySQL as a first-class data backend, and a keyed lookup-hash default — the data layer goes tri-dialect.** This release makes the framework's data layer dialect-portable. `b.sql` is a new chainable query builder that quotes every identifier by construction, binds every value as a placeholder, and emits dialect-correct SQL for SQLite, Postgres, and MySQL; `b.guardSql` validates result rows against NUL bytes, quote-jump sequences, and per-column / total size boundaries. The entire framework data layer — the signed audit chain, cluster leadership and fencing, sessions, break-glass, the local queue, cache, scheduler, migrations, consent, and mail storage — is rebuilt on `b.sql`, which makes MySQL a supported external-database backend alongside Postgres and SQLite. Running the data layer on a real Postgres server surfaced two latent correctness bugs that only a non-SQLite backend exposes: identifiers were emitted unquoted at DDL time but read back camelCase (Postgres folds unquoted names to lowercase, silently breaking the audit chain, consent chain, cluster fencing, and vault-key consistency), and sealed columns coerced Buffer / object payloads through `String()` before encryption (corrupting non-string ciphertext); both are fixed by quoting every identifier and coercing per the column's declared type. Derived-hash columns — the blind-index lookup columns registered through `registerTable` — now default to a keyed MAC (SHAKE256 under the vault's per-deployment MAC key) instead of an unkeyed salted hash, so an exfiltrated database can no longer be brute-forced or rainbow-tabled to recover the indexed values; existing salted-hash indexes are read through a dual-read path and migrated forward, so the change is non-breaking. Audit-signing key rotation now preserves every historical checkpoint (rotation previously stranded checkpoints signed under the prior key). New cross-border data-residency postures (appi-jp, pdpa-sg, uk-gdpr) enforce a mandatory storage vacuum after erasure. Outbound TLS appends classical X25519 to its key-exchange preference so it can complete a handshake with a peer that advertises no post-quantum hybrid — previously the hybrids-only preference failed those handshakes outright — and emits a `tls.classical_downgrade` audit event whenever a connection lands on a classical group. Outbound HTTP/2 negotiation falls back to HTTP/1.1 against servers that only speak h1, the network heartbeat honors a target's permitted protocols instead of pinning cleartext targets down, a WebSocket connection closes cleanly on a peer's TCP half-close instead of wedging open, and the Azure blob backend encodes object-key path segments correctly. **Added:** *b.sql — a dialect-aware chainable SQL builder* — `b.sql` composes SELECT / INSERT / UPDATE / UPSERT / DELETE / DDL from a fluent chain. Every identifier is validated and quoted by construction (`"name"` on SQLite/Postgres, `` `name` `` on MySQL), every value binds as a placeholder rather than interpolating, and the emitted statement is validated as a single balanced, single-statement query before it leaves the builder. Pass `{ dialect: "postgres" | "mysql" | "sqlite" }` (default SQLite) to target a backend; `upsert` emits the dialect-final conflict syntax. It composes `b.safeSql` for the identifier and placeholder primitives, so a SQL string can no longer be assembled by hand inside the framework. · *b.guardSql — result-row output validation* — `b.guardSql` gates query result rows against embedded NUL bytes, quote-jump sequences, and configurable per-column and total-size boundaries, with the standard guard profiles (strict / balanced / permissive) and compliance postures (hipaa / pci-dss / gdpr / soc2). It is the output-side complement to the input-side `b.safeSql`. · *MySQL is a first-class data backend* — MySQL joins Postgres and SQLite as a supported external-database backend. The framework's schema reconciler emits MySQL DDL, and the full data layer — signed audit chain (append + verify), cluster leadership and lease fencing, sessions, break-glass, the local queue, cache, scheduler, migrations, and consent — runs against a real MySQL server. Select the backend at `b.cluster.init` / `b.externalDb` configuration via the `dialect` option; `b.clusterStorage.dialect()` exposes the configured backend dialect to composing code. · *Cross-border erasure postures: appi-jp, pdpa-sg, uk-gdpr* — Three data-residency compliance postures are added (Japan APPI, Singapore PDPA, UK GDPR). Each requires a mandatory storage vacuum after erasure (so deleted rows are reclaimed from the page store, not just tombstoned), a signed audit chain, encrypted backups, and a minimum TLS version. Pin one with `b.compliance.set`. **Fixed:** *Cross-border erasure performs the mandatory vacuum* — Erasure under the uk-gdpr, appi-jp, and pdpa-sg residency postures now runs the storage vacuum the posture mandates, reclaiming erased rows from the page store rather than leaving them recoverable as free-list tombstones. **Security:** *Lookup-hash columns default to a keyed MAC* — Derived-hash (blind-index) columns registered through `registerTable` now default to `derivedHashMode: "hmac-shake256"` — SHAKE256 keyed with the vault's per-deployment MAC key — instead of the previous unkeyed `salted-sha3`. The index value is no longer recomputable from the indexed plaintext alone, so an attacker who exfiltrates the database cannot brute-force or rainbow-table a lookup column (for example a subject-email index) without also holding the vault MAC key (CWE-916 / CWE-759). Existing `salted-sha3` indexes are read through a dual-read path and re-derived on write, so deployments upgrade without re-indexing up front. · *Postgres identifier casing no longer breaks the audit and cluster chains* — Identifiers were written unquoted in DDL but read back in camelCase. On Postgres (which folds an unquoted identifier to lowercase) this silently desynchronized the signed audit chain, the consent chain, cluster leadership and fencing, and vault-key consistency — each reads a column the server had stored under a lowercased name. Every framework identifier is now quoted at both DDL and query time so the stored and read names match on every dialect (CWE-670). SQLite deployments were unaffected and remain byte-compatible. · *Sealed columns preserve non-string payloads* — A sealed column coerced its value through `String()` before encryption, corrupting Buffer and object payloads (a Buffer became `"[object Object]"`-class garbage, an object its `toString`). Sealed values are now encoded per the column's declared type before the seal, so binary and structured payloads round-trip intact (CWE-704). · *Audit-signing key rotation preserves historical checkpoints* — Rotating the audit-signing key stranded every checkpoint signed under the prior key — `verifyCheckpoints` ignored the per-fingerprint history file the rotation writes, so post-rotation verification failed on otherwise-valid historical checkpoints. Verification now resolves each checkpoint's signing key by fingerprint (`getPublicKeyByFingerprint`) across the rotation history, so the full chain verifies after a key rotation. · *Outbound TLS reaches classical-only peers and audits the downgrade* — The framework's outbound TLS offered only ML-KEM hybrid key-exchange groups, so a handshake with a peer that does not advertise a post-quantum hybrid — most of today's internet — failed outright (`handshake_failure`), leaving outbound connections to webhooks, OAuth providers, ACME directories, object stores, and DoT/DoH/SMTP/Redis-over-TLS unable to complete. Classical X25519 is now appended to the group preference so the hybrid is still negotiated whenever the peer supports it, and the connection completes over classical X25519 when it does not. Every connection that lands on a classical group rather than the post-quantum hybrid emits a `tls.classical_downgrade` audit event (carrying the negotiated group) so operators can observe and alert on which peers are not yet post-quantum-capable. · *Transport reachability and correctness* — Outbound HTTP/2 negotiation now falls back to HTTP/1.1 when a TLS server offers only h1 (an ALPN protocol_version alert no longer fails the request). The network heartbeat honors a target's permitted protocols instead of dropping non-default ones, so a cleartext `http://` health target is no longer reported permanently down. A WebSocket connection closes cleanly when a peer half-closes its TCP socket (a bare FIN) instead of wedging the connection open. The Azure blob backend percent-encodes each object-key path segment, so a key containing reserved characters can no longer corrupt the request URL. **Migration:** *Derived-hash default change is non-breaking; MySQL is opt-in* — Lookup-hash columns default to the keyed MAC on new writes and migrate existing rows on access through the dual-read path — no upfront re-indexing, no operator action required. To pin the previous unkeyed index (for example to keep a column byte-compatible with an external system), pass `derivedHashMode: "salted-sha3"` to `registerTable`. MySQL as a data backend is opt-in: existing SQLite and Postgres deployments are unchanged unless you set `dialect: "mysql"`. The Postgres identifier-casing and sealed-column-coercion fixes change the emitted DDL and the at-rest encoding of non-string sealed values; a Postgres deployment created before this release reconciles its schema to the quoted identifiers on the next schema-ensure pass.
|
package/lib/db-schema.js
CHANGED
|
@@ -479,8 +479,8 @@ function keyTextType(database) {
|
|
|
479
479
|
// it throws "syntax error at PRAGMA" on the others). The table name binds
|
|
480
480
|
// as a `?` parameter (never concatenated into the SQL text), so an operator
|
|
481
481
|
// table name with metacharacters can't break the introspection query. On
|
|
482
|
-
// Postgres the
|
|
483
|
-
//
|
|
482
|
+
// Postgres / MySQL the introspection is confined to current_schema() /
|
|
483
|
+
// DATABASE() (where the bare-named CREATE TABLE lands); an operator running
|
|
484
484
|
// multiple schemas qualifies via the `schema.table` handle convention
|
|
485
485
|
// elsewhere — listColumns reconciles by bare name here, matching the
|
|
486
486
|
// reconciler's CREATE TABLE (which is also bare-named).
|
|
@@ -498,9 +498,24 @@ function listColumns(database, tableName) {
|
|
|
498
498
|
// information_schema.columns view (a schema-qualified system table b.sql's
|
|
499
499
|
// verb builders don't model); the ONLY value (table name) binds as a `?`,
|
|
500
500
|
// every column/table reference is a static literal — no injection surface.
|
|
501
|
+
// The schema predicate confines introspection to the schema/database the
|
|
502
|
+
// reconciler's bare-named CREATE TABLE actually writes into (Postgres
|
|
503
|
+
// current_schema() = the first writable schema on the search_path; MySQL
|
|
504
|
+
// DATABASE() = the connection's default database). Without it a same-named
|
|
505
|
+
// table in another schema/database pollutes the column set - silently skipping
|
|
506
|
+
// a needed ADD COLUMN or fabricating a drift "extra" that refuses a regulated-
|
|
507
|
+
// posture boot. Both are zero-arg SQL functions in predicate position, so the
|
|
508
|
+
// table name stays the single bound parameter (no new placeholder).
|
|
509
|
+
// Two fully-static introspection strings, one per dialect: DATABASE() /
|
|
510
|
+
// current_schema() are SQL functions baked into the literal (never a
|
|
511
|
+
// concatenated value), so the only bound value remains the table name `?`.
|
|
501
512
|
// allow:hand-rolled-sql — static information_schema introspection, single bound param
|
|
502
|
-
var infoSql =
|
|
503
|
-
|
|
513
|
+
var infoSql = dialect === "mysql"
|
|
514
|
+
? "SELECT column_name FROM information_schema.columns " +
|
|
515
|
+
"WHERE table_schema = DATABASE() AND table_name = ?"
|
|
516
|
+
// allow:hand-rolled-sql — Postgres branch, same static-introspection shape
|
|
517
|
+
: "SELECT column_name FROM information_schema.columns " +
|
|
518
|
+
"WHERE table_schema = current_schema() AND table_name = ?";
|
|
504
519
|
var stmt = database.prepare(infoSql);
|
|
505
520
|
var irows = stmt.all.apply(stmt, [tableName]);
|
|
506
521
|
for (var j = 0; j < irows.length; j++) {
|
package/lib/network-proxy.js
CHANGED
|
@@ -20,6 +20,11 @@ var DEFAULT_HTTPS_PORT = 443; // RFC 9110 §4.2.2
|
|
|
20
20
|
var DEFAULT_HTTP_PORT = C.BYTES.bytes(80); // RFC 9110 §4.2.1
|
|
21
21
|
|
|
22
22
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
23
|
+
// Lazy so pqc-agent's TLS/audit graph isn't pulled into every process that
|
|
24
|
+
// imports network-proxy but never proxies an https upstream. Used only to audit
|
|
25
|
+
// a classical-group fallback on a proxy-tunneled TLS handshake (the direct path
|
|
26
|
+
// audits in pqc-agent.create()).
|
|
27
|
+
var pqcAgent = lazyRequire(function () { return require("./pqc-agent"); });
|
|
23
28
|
|
|
24
29
|
var STATE = {
|
|
25
30
|
http: null,
|
|
@@ -167,6 +172,13 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
|
|
|
167
172
|
function done(err, sock) { if (settled) return; settled = true; callback(err, sock); }
|
|
168
173
|
proxySocket.on("error", function (e) { done(e); });
|
|
169
174
|
proxySocket.on(proxyUrl.protocol === "https:" ? "secureConnect" : "connect", function () {
|
|
175
|
+
if (proxyUrl.protocol === "https:") {
|
|
176
|
+
// The CONNECT-tunnel leg to an https proxy is itself a TLS handshake;
|
|
177
|
+
// audit a classical fallback to the proxy too, not only to the upstream.
|
|
178
|
+
pqcAgent()._auditClassicalDowngrade(proxySocket, {
|
|
179
|
+
host: proxyUrl.hostname, port: proxyPort,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
170
182
|
var lines = [
|
|
171
183
|
"CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1",
|
|
172
184
|
"Host: " + targetHost + ":" + targetPort,
|
|
@@ -218,7 +230,18 @@ function agentFor(targetUrl) {
|
|
|
218
230
|
minVersion: "TLSv1.3",
|
|
219
231
|
ecdhCurve: C.TLS_GROUP_CURVE_STR,
|
|
220
232
|
ALPNProtocols: options.ALPNProtocols,
|
|
221
|
-
}, function () {
|
|
233
|
+
}, function () {
|
|
234
|
+
// Audit a classical-group fallback on the upstream (target) handshake
|
|
235
|
+
// reached through the proxy tunnel, so the "every outbound TLS path
|
|
236
|
+
// emits tls.classical_downgrade" guarantee holds for proxied requests
|
|
237
|
+
// too (the direct path audits in pqc-agent.create). Drop-silent; the
|
|
238
|
+
// handshake itself is unchanged (still hybrid-preferred TLSv1.3).
|
|
239
|
+
pqcAgent()._auditClassicalDowngrade(secure, {
|
|
240
|
+
host: options.servername || options.host,
|
|
241
|
+
port: options.port,
|
|
242
|
+
});
|
|
243
|
+
cb(null, secure);
|
|
244
|
+
});
|
|
222
245
|
secure.on("error", function (e) { cb(e); });
|
|
223
246
|
});
|
|
224
247
|
};
|
package/lib/object-store/gcs.js
CHANGED
|
@@ -364,7 +364,10 @@ function create(config) {
|
|
|
364
364
|
// Build the canonical request — identical shape to SigV4. The
|
|
365
365
|
// sigv4 export gives us the formatter so this stays in lockstep
|
|
366
366
|
// with the AWS implementation.
|
|
367
|
-
|
|
367
|
+
// GCS's V4 signature, like S3, URI-encodes the canonical path ONCE; the key
|
|
368
|
+
// is already single-encoded into url.pathname above, so pass doubleEncodePath
|
|
369
|
+
// = false (a second encode would 403 any key with a space/+/&/unicode).
|
|
370
|
+
var canon = sigv4.canonicalRequest(method, url, headers, "UNSIGNED-PAYLOAD", false);
|
|
368
371
|
var stringToSign = [
|
|
369
372
|
GCS_V4_ALGORITHM,
|
|
370
373
|
amzDate,
|
|
@@ -514,8 +514,11 @@ function create(config) {
|
|
|
514
514
|
function _objectUrl(name, key, query) {
|
|
515
515
|
// Each key segment is encoded individually so that legitimate "/"
|
|
516
516
|
// separators in the key are preserved (S3 treats keys with slashes
|
|
517
|
-
// as flat names, not directories).
|
|
518
|
-
|
|
517
|
+
// as flat names, not directories). Use sigv4.awsUriEncode (not
|
|
518
|
+
// encodeURIComponent, which leaves !*'() unescaped) so the wire path
|
|
519
|
+
// matches the bytes S3 canonicalizes the signature over — same encoder
|
|
520
|
+
// _keyToUrl uses for the put/get path.
|
|
521
|
+
var encKey = key.split("/").map(function (s) { return sigv4.awsUriEncode(s, true); }).join("/");
|
|
519
522
|
var uo = _internalUrl(endpoint, allowedProtocols);
|
|
520
523
|
if (pathStyle) {
|
|
521
524
|
uo.pathname = "/" + name + "/" + encKey;
|
|
@@ -86,9 +86,16 @@ function hmacSha256(key, data) {
|
|
|
86
86
|
// AWS-style URI encoding: same as RFC 3986 except path '/' may be preserved.
|
|
87
87
|
function awsUriEncode(str, encodeSlash) {
|
|
88
88
|
var out = "";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// Iterate by Unicode code point, not UTF-16 code unit. Array.from() keeps a
|
|
90
|
+
// non-BMP character's surrogate pair together (one element), so a key like
|
|
91
|
+
// "photo-<U+1F600>.jpg" encodes as a single UTF-8 sequence. Iterating by
|
|
92
|
+
// index would hand encodeURIComponent a lone surrogate, which throws
|
|
93
|
+
// "URIError: URI malformed". Output is byte-for-byte identical for the
|
|
94
|
+
// BMP/ASCII keys that are the overwhelming common case.
|
|
95
|
+
var cps = Array.from(str);
|
|
96
|
+
for (var i = 0; i < cps.length; i++) {
|
|
97
|
+
var ch = cps[i];
|
|
98
|
+
var c = ch.codePointAt(0);
|
|
92
99
|
if ((c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) ||
|
|
93
100
|
(c >= 0x30 && c <= 0x39) ||
|
|
94
101
|
ch === "-" || ch === "_" || ch === "." || ch === "~") {
|
|
@@ -137,13 +144,24 @@ function canonicalHeaders(headers) {
|
|
|
137
144
|
return { canonical: canon, signed: signed.join(";") };
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
// AWS SigV4 canonical URI. Per the SigV4 spec, S3 (and S3-compatible stores +
|
|
148
|
+
// GCS's V4) URI-encode the path EXACTLY ONCE; every other AWS service (sqs,
|
|
149
|
+
// logs, sns, ...) encodes it TWICE. Callers build urlObj through the WHATWG URL
|
|
150
|
+
// parser, so urlObj.pathname is ALREADY the single-encoded wire form (a key
|
|
151
|
+
// "a b.txt" is "/a%20b.txt") and the request sends that pathname verbatim. So
|
|
152
|
+
// for S3/GCS the canonical path MUST equal the pathname as-is: a second
|
|
153
|
+
// awsUriEncode would sign "/a%2520b.txt", a path the wire never carries, giving
|
|
154
|
+
// SignatureDoesNotMatch (403) for any key with a space/+/&/unicode. For the
|
|
155
|
+
// double-encode services the second pass is the spec requirement. signRequest
|
|
156
|
+
// derives doubleEncodePath from the service; GCS's V4 signer passes false.
|
|
157
|
+
function canonicalRequest(method, urlObj, headers, payloadHash, doubleEncodePath) {
|
|
141
158
|
var canonHeaders = canonicalHeaders(headers);
|
|
142
159
|
var path = urlObj.pathname;
|
|
143
160
|
if (!path) path = "/";
|
|
161
|
+
var canonicalPath = doubleEncodePath ? awsUriEncode(path, false) : path;
|
|
144
162
|
return [
|
|
145
163
|
method.toUpperCase(),
|
|
146
|
-
|
|
164
|
+
canonicalPath,
|
|
147
165
|
canonicalQueryString(urlObj.searchParams),
|
|
148
166
|
canonHeaders.canonical,
|
|
149
167
|
canonHeaders.signed,
|
|
@@ -204,7 +222,11 @@ function signRequest(opts) {
|
|
|
204
222
|
headers["x-amz-security-token"] = opts.sessionToken;
|
|
205
223
|
}
|
|
206
224
|
|
|
207
|
-
|
|
225
|
+
// S3 single-encodes the canonical path; every other AWS service double-encodes
|
|
226
|
+
// it (see canonicalRequest). The path itself is "/" for the non-S3 callers
|
|
227
|
+
// (cloudwatch/sqs put params in the query or body), so this only changes the
|
|
228
|
+
// wire result for S3, where it fixes the long-standing double-encode 403.
|
|
229
|
+
var canon = canonicalRequest(opts.method, url, headers, opts.payloadHash, service !== "s3");
|
|
208
230
|
var credentialScope = dateStamp + "/" + opts.region + "/" + service + "/aws4_request";
|
|
209
231
|
var sts = stringToSign(amzDate, credentialScope, canon);
|
|
210
232
|
var signingKey = deriveSigningKey(opts.secretAccessKey, dateStamp, opts.region, service);
|
package/lib/sql.js
CHANGED
|
@@ -229,9 +229,17 @@ function _ddlType(logical, dialect) {
|
|
|
229
229
|
if (key === "JSON") {
|
|
230
230
|
return dialect === "postgres" ? "JSONB" : (dialect === "mysql" ? "JSON" : "TEXT");
|
|
231
231
|
}
|
|
232
|
-
// Unrecognised: a verbatim dialect-specific type
|
|
233
|
-
//
|
|
234
|
-
//
|
|
232
|
+
// Unrecognised: a verbatim dialect-specific type (VARCHAR(255), GEOGRAPHY,
|
|
233
|
+
// NUMERIC(10,2), DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, MySQL
|
|
234
|
+
// ENUM('a','b') / SET(...), ...). It follows a quoted identifier so it is in
|
|
235
|
+
// type position, never identifier position. Injection safety for the type
|
|
236
|
+
// token is enforced at the statement level: createTable / alterTable route
|
|
237
|
+
// the finished DDL through _assertCatalogEmittable, whose quote-aware
|
|
238
|
+
// single-statement scan refuses a top-level ';', a comment marker, an
|
|
239
|
+
// unbalanced quote, an unbalanced paren, and a NUL - while CORRECTLY allowing
|
|
240
|
+
// those same characters when they sit inside a balanced quoted label (e.g.
|
|
241
|
+
// ENUM('needs;review')). A non-quote-aware pre-scan here would over-reject
|
|
242
|
+
// such valid labels, so the one quote-aware gate is the right place to check.
|
|
235
243
|
return logical;
|
|
236
244
|
}
|
|
237
245
|
|
|
@@ -2486,8 +2494,25 @@ class UpsertBuilder extends Builder {
|
|
|
2486
2494
|
throw _err("upsert readback: conflict key '" + keys[i] + "' is not in the value set",
|
|
2487
2495
|
"sql-builder/bad-conflict");
|
|
2488
2496
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2497
|
+
var keyVal = this._values[idx];
|
|
2498
|
+
if (keyVal instanceof SqlFunction) {
|
|
2499
|
+
// A server-evaluated function (NOW() / CURRENT_TIMESTAMP / ...) as a
|
|
2500
|
+
// conflict key has no stable readback identity: the row holds the value
|
|
2501
|
+
// the server computed at INSERT time, which a fresh evaluation in this
|
|
2502
|
+
// WHERE would never equal, so the readback would silently match zero
|
|
2503
|
+
// rows. Refuse rather than return a wrong (empty) result.
|
|
2504
|
+
throw _err("upsert readback: conflict key '" + keys[i] + "' is a " +
|
|
2505
|
+
"server-evaluated function (b.sql.fn) with no stable readback identity " +
|
|
2506
|
+
"- use a literal/cast conflict key or read the row back explicitly",
|
|
2507
|
+
"sql-builder/bad-conflict");
|
|
2508
|
+
}
|
|
2509
|
+
// Resolve the key value through the same cell renderer the VALUES tuple
|
|
2510
|
+
// uses, so a b.sql.cast(...) conflict key emits `col = CAST(? AS type)`
|
|
2511
|
+
// (Postgres `col = ?::type`) binding the inner value, and a plain scalar
|
|
2512
|
+
// still emits `col = ?` binding the value unchanged.
|
|
2513
|
+
var cell = _renderValueCell(keyVal, dialect);
|
|
2514
|
+
conds.push(_quoteId(keys[i], dialect) + " = " + cell.sql);
|
|
2515
|
+
for (var cp = 0; cp < cell.params.length; cp++) params.push(cell.params[cp]);
|
|
2491
2516
|
}
|
|
2492
2517
|
sql += " WHERE " + conds.join(" AND ");
|
|
2493
2518
|
return { sql: sql, params: params };
|
|
@@ -2614,6 +2639,20 @@ function createTable(name, columns, opts) {
|
|
|
2614
2639
|
return def;
|
|
2615
2640
|
});
|
|
2616
2641
|
if (Array.isArray(opts.primaryKey) && opts.primaryKey.length > 0) {
|
|
2642
|
+
// A column-level primary key (primaryKey / autoIncrement / serial) and a
|
|
2643
|
+
// composite opts.primaryKey are mutually exclusive: emitting both produces
|
|
2644
|
+
// two PRIMARY KEY clauses, which sqlite / Postgres / MySQL all reject at the
|
|
2645
|
+
// driver. Catch the contradiction at build time with a clear error rather
|
|
2646
|
+
// than a cryptic "multiple primary keys" failure mid-migration. Lives in the
|
|
2647
|
+
// shared composer so defineTable is covered too.
|
|
2648
|
+
var colHasPk = columns.some(function (c) {
|
|
2649
|
+
return c && (c.primaryKey || c.autoIncrement || c.serial);
|
|
2650
|
+
});
|
|
2651
|
+
if (colHasPk) {
|
|
2652
|
+
throw _err("createTable: a column-level primary key (primaryKey / " +
|
|
2653
|
+
"autoIncrement / serial) and a composite opts.primaryKey are mutually " +
|
|
2654
|
+
"exclusive", "sql-builder/bad-column");
|
|
2655
|
+
}
|
|
2617
2656
|
opts.primaryKey.forEach(_validateColumn);
|
|
2618
2657
|
pieces.push("PRIMARY KEY (" + opts.primaryKey.map(function (k) {
|
|
2619
2658
|
return _quoteId(k, dialect);
|
|
@@ -2621,7 +2660,11 @@ function createTable(name, columns, opts) {
|
|
|
2621
2660
|
}
|
|
2622
2661
|
var ifNot = opts.ifNotExists === false ? "" : "IF NOT EXISTS ";
|
|
2623
2662
|
var sql = "CREATE TABLE " + ifNot + ref.ref(dialect) + " (" + pieces.join(", ") + ")";
|
|
2624
|
-
|
|
2663
|
+
// Route the finished DDL through the same emittable gate every SELECT /
|
|
2664
|
+
// INSERT / UPDATE / DELETE verb uses: it refuses a stacked top-level ';', a
|
|
2665
|
+
// NUL, an unterminated quote, and unbalanced parens - a defence-in-depth
|
|
2666
|
+
// backstop behind the per-column type / constraint guards.
|
|
2667
|
+
return _assertCatalogEmittable(sql, []);
|
|
2625
2668
|
}
|
|
2626
2669
|
|
|
2627
2670
|
// DDL DEFAULT renderer - numeric / boolean / null inline; a string
|
|
@@ -2767,11 +2810,11 @@ function alterTable(name, change, opts) {
|
|
|
2767
2810
|
if (col.notNull) def += " NOT NULL";
|
|
2768
2811
|
if (col.unique) def += " UNIQUE";
|
|
2769
2812
|
if (col.default !== undefined) def += " DEFAULT " + _ddlDefault(col.default);
|
|
2770
|
-
return
|
|
2813
|
+
return _assertCatalogEmittable(head + "ADD COLUMN " + def, []);
|
|
2771
2814
|
}
|
|
2772
2815
|
if (change.dropColumn) {
|
|
2773
2816
|
_validateColumn(change.dropColumn);
|
|
2774
|
-
return
|
|
2817
|
+
return _assertCatalogEmittable(head + "DROP COLUMN " + _quoteId(change.dropColumn, dialect), []);
|
|
2775
2818
|
}
|
|
2776
2819
|
if (change.renameColumn) {
|
|
2777
2820
|
var rc = change.renameColumn;
|
|
@@ -2780,10 +2823,8 @@ function alterTable(name, change, opts) {
|
|
|
2780
2823
|
}
|
|
2781
2824
|
_validateColumn(rc.from);
|
|
2782
2825
|
_validateColumn(rc.to);
|
|
2783
|
-
return
|
|
2784
|
-
|
|
2785
|
-
params: [],
|
|
2786
|
-
};
|
|
2826
|
+
return _assertCatalogEmittable(
|
|
2827
|
+
head + "RENAME COLUMN " + _quoteId(rc.from, dialect) + " TO " + _quoteId(rc.to, dialect), []);
|
|
2787
2828
|
}
|
|
2788
2829
|
throw _err("alterTable change must be addColumn / dropColumn / renameColumn",
|
|
2789
2830
|
"sql-builder/bad-alter");
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:43ee488f-aee3-40ee-8d42-ed3f547dcbe3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-12T15:44:45.267Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.15.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.3",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.3",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.15.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.3",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.15.
|
|
57
|
+
"ref": "@blamejs/core@0.15.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|