velocious 1.0.571 → 1.0.573

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 (80) hide show
  1. package/README.md +2 -0
  2. package/build/background-jobs/web/authorization.js +2 -33
  3. package/build/background-jobs/web/path-matcher.js +5 -30
  4. package/build/database/drivers/base.js +17 -0
  5. package/build/database/drivers/mssql/index.js +30 -0
  6. package/build/database/record/index.js +22 -10
  7. package/build/deployment-api/controller.js +437 -0
  8. package/build/deployment-api/index.js +210 -0
  9. package/build/deployment-api/path-matcher.js +45 -0
  10. package/build/deployment-api/registry.js +84 -0
  11. package/build/deployment-api/run-store.js +798 -0
  12. package/build/deployment-api/sanitize.js +114 -0
  13. package/build/http-client/request.js +3 -1
  14. package/build/src/background-jobs/web/authorization.d.ts.map +1 -1
  15. package/build/src/background-jobs/web/authorization.js +3 -29
  16. package/build/src/background-jobs/web/path-matcher.d.ts +2 -12
  17. package/build/src/background-jobs/web/path-matcher.d.ts.map +1 -1
  18. package/build/src/background-jobs/web/path-matcher.js +5 -30
  19. package/build/src/database/drivers/base.d.ts +16 -0
  20. package/build/src/database/drivers/base.d.ts.map +1 -1
  21. package/build/src/database/drivers/base.js +16 -1
  22. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  23. package/build/src/database/drivers/mssql/index.js +29 -1
  24. package/build/src/database/record/index.d.ts +3 -2
  25. package/build/src/database/record/index.d.ts.map +1 -1
  26. package/build/src/database/record/index.js +21 -10
  27. package/build/src/deployment-api/controller.d.ts +117 -0
  28. package/build/src/deployment-api/controller.d.ts.map +1 -0
  29. package/build/src/deployment-api/controller.js +384 -0
  30. package/build/src/deployment-api/index.d.ts +46 -0
  31. package/build/src/deployment-api/index.d.ts.map +1 -0
  32. package/build/src/deployment-api/index.js +178 -0
  33. package/build/src/deployment-api/path-matcher.d.ts +31 -0
  34. package/build/src/deployment-api/path-matcher.d.ts.map +1 -0
  35. package/build/src/deployment-api/path-matcher.js +39 -0
  36. package/build/src/deployment-api/registry.d.ts +106 -0
  37. package/build/src/deployment-api/registry.d.ts.map +1 -0
  38. package/build/src/deployment-api/registry.js +74 -0
  39. package/build/src/deployment-api/run-store.d.ts +402 -0
  40. package/build/src/deployment-api/run-store.d.ts.map +1 -0
  41. package/build/src/deployment-api/run-store.js +711 -0
  42. package/build/src/deployment-api/sanitize.d.ts +27 -0
  43. package/build/src/deployment-api/sanitize.d.ts.map +1 -0
  44. package/build/src/deployment-api/sanitize.js +100 -0
  45. package/build/src/http-client/request.d.ts.map +1 -1
  46. package/build/src/http-client/request.js +4 -2
  47. package/build/src/sync/signed-sync-envelope-replay-service.d.ts +154 -0
  48. package/build/src/sync/signed-sync-envelope-replay-service.d.ts.map +1 -0
  49. package/build/src/sync/signed-sync-envelope-replay-service.js +294 -0
  50. package/build/src/sync/sync-envelope-replay-service.d.ts +84 -15
  51. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  52. package/build/src/sync/sync-envelope-replay-service.js +98 -16
  53. package/build/src/utils/bearer-token.d.ts +15 -0
  54. package/build/src/utils/bearer-token.d.ts.map +1 -0
  55. package/build/src/utils/bearer-token.js +29 -0
  56. package/build/src/utils/mount-prefix.d.ts +21 -0
  57. package/build/src/utils/mount-prefix.d.ts.map +1 -0
  58. package/build/src/utils/mount-prefix.js +35 -0
  59. package/build/sync/signed-sync-envelope-replay-service.js +342 -0
  60. package/build/sync/sync-envelope-replay-service.js +110 -15
  61. package/build/tsconfig.tsbuildinfo +1 -1
  62. package/build/utils/bearer-token.js +34 -0
  63. package/build/utils/mount-prefix.js +36 -0
  64. package/package.json +2 -1
  65. package/src/background-jobs/web/authorization.js +2 -33
  66. package/src/background-jobs/web/path-matcher.js +5 -30
  67. package/src/database/drivers/base.js +17 -0
  68. package/src/database/drivers/mssql/index.js +30 -0
  69. package/src/database/record/index.js +22 -10
  70. package/src/deployment-api/controller.js +437 -0
  71. package/src/deployment-api/index.js +210 -0
  72. package/src/deployment-api/path-matcher.js +45 -0
  73. package/src/deployment-api/registry.js +84 -0
  74. package/src/deployment-api/run-store.js +798 -0
  75. package/src/deployment-api/sanitize.js +114 -0
  76. package/src/http-client/request.js +3 -1
  77. package/src/sync/signed-sync-envelope-replay-service.js +342 -0
  78. package/src/sync/sync-envelope-replay-service.js +110 -15
  79. package/src/utils/bearer-token.js +34 -0
  80. package/src/utils/mount-prefix.js +36 -0
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  * Declarative client sync scopes with per-scope cursors, automatic mutation tracking, realtime delivery, and `sync`/`pull` progress reporting for "X of Y" import screens (see [docs/sync-client.md](docs/sync-client.md))
16
16
  * Reactive `useLiveQuery(Model.where(...))` queries that stay current from committed local model changes across local writes, pulls, and realtime (see [docs/live-queries.md](docs/live-queries.md))
17
17
  * Server-side sync envelope replay orchestration for app-owned sync receivers (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
18
+ * AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see [docs/awesome-tasks-offline-sync-proof.md](docs/awesome-tasks-offline-sync-proof.md))
18
19
  * SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see [docs/sqlite-web-persistence.md](docs/sqlite-web-persistence.md))
19
20
  * Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
20
21
  * Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
@@ -29,6 +30,7 @@
29
30
  * Default-on buffered HTTP response compression with Brotli/gzip content negotiation, global and per-response opt-outs, and HEAD-correct representation headers (see [docs/http-server.md](docs/http-server.md#response-compression))
30
31
  * Background jobs with failure events for production reporting and authorized database-scoped dashboard count snapshots/deltas (see [docs/background-jobs.md](docs/background-jobs.md) and [docs/background-jobs-dashboard.md](docs/background-jobs-dashboard.md))
31
32
  * Durable one-off background-job scheduling with exact epoch timestamps (see [docs/scheduled-background-job-enqueue.md](docs/scheduled-background-job-enqueue.md))
33
+ * Mountable authenticated callable deployment API with allowlisted projects/stages, immutable-revision validation, idempotent runs, duplicate-safe interruption reconciliation, and adapter-owned execution for integrations like Rampway (see [docs/deployment-api.md](docs/deployment-api.md))
32
34
  * Rails-style request and database query logging (see [docs/logging.md](docs/logging.md))
33
35
  * EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
34
36
  * Trusted reverse proxy handling for `request.remoteAddress()` (see [docs/trusted-proxies.md](docs/trusted-proxies.md))
@@ -1,37 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import crypto from "node:crypto"
4
-
5
- /**
6
- * Constant-time comparison so token checks don't leak length/contents through
7
- * timing. Returns false for differing lengths before the timing-safe compare.
8
- * @param {string} a - First value.
9
- * @param {string} b - Second value.
10
- * @returns {boolean} - Whether the values are equal.
11
- */
12
- function safeEqual(a, b) {
13
- const bufferA = Buffer.from(String(a))
14
- const bufferB = Buffer.from(String(b))
15
-
16
- if (bufferA.length !== bufferB.length) return false
17
-
18
- return crypto.timingSafeEqual(bufferA, bufferB)
19
- }
20
-
21
- /**
22
- * Runs bearer token.
23
- * @param {import("../../http-server/client/request.js").default | import("../../http-server/client/websocket-request.js").default} request - Request object.
24
- * @returns {string | null} - Bearer token from the Authorization header, if any.
25
- */
26
- function bearerToken(request) {
27
- const header = request.header("authorization")
28
-
29
- if (typeof header !== "string") return null
30
-
31
- const match = header.match(/^Bearer\s+(.+)$/i)
32
-
33
- return match ? match[1].trim() : null
34
- }
3
+ import {bearerToken, constantTimeEqual} from "../../utils/bearer-token.js"
35
4
 
36
5
  /**
37
6
  * Runs is loopback.
@@ -72,7 +41,7 @@ export async function authorizeJobsRequest({ability, configuration, options, req
72
41
 
73
42
  if (accessTokens.length > 0 && token) {
74
43
  for (const accessToken of accessTokens) {
75
- if (safeEqual(token, accessToken)) return true
44
+ if (constantTimeEqual(token, accessToken)) return true
76
45
  }
77
46
  }
78
47
 
@@ -1,28 +1,13 @@
1
1
  // @ts-check
2
2
 
3
+ import {mountSubPath, normalizeMountPrefix} from "../../utils/mount-prefix.js"
4
+
3
5
  /**
4
6
  * @typedef {object} JobsApiMatch
5
7
  * @property {string} action - Controller action to run.
6
8
  * @property {Record<string, string>} params - Extra params extracted from the path.
7
9
  */
8
-
9
- /**
10
- * Normalizes a mount prefix: ensures a leading slash and strips any trailing
11
- * slash so `/velocious/jobs/` and `/velocious/jobs` behave identically.
12
- * @param {string} at - Raw mount prefix.
13
- * @returns {string} - Normalized prefix.
14
- */
15
- export function normalizeMountPrefix(at) {
16
- if (typeof at !== "string" || !at.startsWith("/")) {
17
- throw new Error(`mount requires an 'at' path starting with '/', got: ${String(at)}`)
18
- }
19
-
20
- if (at.length > 1 && at.endsWith("/")) {
21
- return at.slice(0, -1)
22
- }
23
-
24
- return at
25
- }
10
+ export {normalizeMountPrefix}
26
11
 
27
12
  /**
28
13
  * Matches an incoming request against the read-only jobs API routes that live
@@ -35,19 +20,9 @@ export function normalizeMountPrefix(at) {
35
20
  * @returns {JobsApiMatch | null} - Matched action or null.
36
21
  */
37
22
  export function matchJobsApiPath({prefix, path, method}) {
38
- /** @type {string} */
39
- let subPath
23
+ const subPath = mountSubPath({prefix, path})
40
24
 
41
- if (prefix === "/") {
42
- // Root mount: the whole path is the sub-path (avoid building a "//" guard).
43
- subPath = path
44
- } else if (path === prefix) {
45
- subPath = "/"
46
- } else if (path.startsWith(`${prefix}/`)) {
47
- subPath = path.slice(prefix.length)
48
- } else {
49
- return null
50
- }
25
+ if (subPath === null) return null
51
26
 
52
27
  if (method === "GET" && subPath === "/api/health") return {action: "health", params: {}}
53
28
  if (method === "GET" && subPath === "/api/stats") return {action: "stats", params: {}}
@@ -961,6 +961,23 @@ export default class VelociousDatabaseDriversBase {
961
961
  */
962
962
  supportsDefaultPrimaryKeyUUID() { return false }
963
963
 
964
+ /**
965
+ * Executes an insert that carries an explicit primary-key value
966
+ * (client-generated offline-sync ids). Drivers whose auto-increment columns
967
+ * reject explicit values (MSSQL IDENTITY) override this to run the insert
968
+ * with identity insert enabled in a single request.
969
+ * @param {object} args - Options object.
970
+ * @param {QueryOptions} args.options - Query options for the standard query path.
971
+ * @param {string} args.sql - Generated insert SQL.
972
+ * @param {string} args.tableName - Table being inserted into.
973
+ * @returns {Promise<QueryResultType>} - Insert result.
974
+ */
975
+ async insertWithExplicitPrimaryKey({options, sql, tableName}) {
976
+ void tableName
977
+
978
+ return await this.query(sql, options)
979
+ }
980
+
964
981
  /**
965
982
  * Runs supports insert into returning.
966
983
  * @abstract
@@ -362,6 +362,36 @@ export default class VelociousDatabaseDriversMssql extends Base{
362
362
  shouldSetAutoIncrementWhenPrimaryKey() { return true }
363
363
  supportsDefaultPrimaryKeyUUID() { return true }
364
364
 
365
+ /**
366
+ * Runs an explicit primary-key insert as one batch request: SQL Server scopes
367
+ * IDENTITY_INSERT to the session, and node-mssql pool-backed requests may use
368
+ * a different physical session per query, so enabling it in a separate query
369
+ * can leave the actual INSERT on another session. A single batch keeps the
370
+ * whole sequence on one session by construction: enable, insert, disable on
371
+ * success, and a CATCH that disables and rethrows the original error.
372
+ * @param {object} args - Options object.
373
+ * @param {import("../base.js").QueryOptions} args.options - Query options for the standard query path.
374
+ * @param {string} args.sql - Generated insert SQL.
375
+ * @param {string} args.tableName - Table being inserted into.
376
+ * @returns {Promise<import("../base.js").QueryResultType>} - Insert result.
377
+ */
378
+ async insertWithExplicitPrimaryKey({options, sql, tableName}) {
379
+ const quotedTable = this.quoteTable(tableName)
380
+ const batch = [
381
+ `SET IDENTITY_INSERT ${quotedTable} ON;`,
382
+ "BEGIN TRY",
383
+ `${sql};`,
384
+ `SET IDENTITY_INSERT ${quotedTable} OFF;`,
385
+ "END TRY",
386
+ "BEGIN CATCH",
387
+ `SET IDENTITY_INSERT ${quotedTable} OFF;`,
388
+ "THROW;",
389
+ "END CATCH"
390
+ ].join("\n")
391
+
392
+ return await this.query(batch, options)
393
+ }
394
+
365
395
  /**
366
396
  * Runs escape.
367
397
  * @param {?} value - Value to use.
@@ -4248,15 +4248,21 @@ class VelociousDatabaseRecord {
4248
4248
  * @returns {Promise<void>} - Resolves when complete.
4249
4249
  */
4250
4250
  async _createNewRecord() {
4251
- if (!this.getModelClass().connection()["insertSql"]) {
4252
- throw new Error(`No insertSql on ${this.getModelClass().connection().constructor.name}`)
4251
+ // Resolve the connection once and pin the whole insert path to it: a pool
4252
+ // can resolve a different current connection across the awaits below, and
4253
+ // the identity-insert wrapper is only effective on the exact session that
4254
+ // ran SET IDENTITY_INSERT.
4255
+ const connection = this._connection()
4256
+
4257
+ if (!connection["insertSql"]) {
4258
+ throw new Error(`No insertSql on ${connection.constructor.name}`)
4253
4259
  }
4254
4260
 
4255
4261
  const data = Object.assign({}, this._belongsToChanges(), this.rawAttributes())
4256
4262
  const primaryKey = this.getModelClass().primaryKey()
4257
4263
  const primaryKeyColumn = this.getModelClass().getColumns().find((column) => column.getName() == primaryKey)
4258
4264
  const primaryKeyType = primaryKeyColumn?.getType()?.toLowerCase()
4259
- const driverSupportsDefaultUUID = typeof this._connection().supportsDefaultPrimaryKeyUUID == "function" && this._connection().supportsDefaultPrimaryKeyUUID()
4265
+ const driverSupportsDefaultUUID = typeof connection.supportsDefaultPrimaryKeyUUID == "function" && connection.supportsDefaultPrimaryKeyUUID()
4260
4266
  const isUUIDPrimaryKey = primaryKeyType?.includes("uuid")
4261
4267
  const shouldAssignUUIDPrimaryKey = isUUIDPrimaryKey && !driverSupportsDefaultUUID
4262
4268
  this._setDefaultTimestampValues(data)
@@ -4270,14 +4276,20 @@ class VelociousDatabaseRecord {
4270
4276
 
4271
4277
  this._normalizeDateValuesForWrite(data)
4272
4278
 
4273
- const sql = this._connection().insertSql({
4279
+ const sql = connection.insertSql({
4274
4280
  returnLastInsertedColumnNames: columnNames,
4275
4281
  tableName: this._tableName(),
4276
4282
  data
4277
4283
  })
4278
- const insertResult = await this._connection().query(sql, {logName: `${this.getModelClass().name} Create`})
4279
-
4280
- await this._applyInsertResult({data, insertResult, primaryKey})
4284
+ const insertOptions = {logName: `${this.getModelClass().name} Create`}
4285
+ // Explicit primary-key inserts into auto-increment columns go through the
4286
+ // driver's explicit-primary-key insert (MSSQL wraps it in IDENTITY_INSERT);
4287
+ // everything else uses the plain query path.
4288
+ const insertResult = hasUserProvidedPrimaryKey && primaryKeyColumn?.getAutoIncrement() === true
4289
+ ? await connection.insertWithExplicitPrimaryKey({options: insertOptions, sql, tableName: this._tableName()})
4290
+ : await connection.query(sql, insertOptions)
4291
+
4292
+ await this._applyInsertResult({connection, data, insertResult, primaryKey})
4281
4293
  this.setIsNewRecord(false)
4282
4294
 
4283
4295
  this._markLoadedRelationshipsPreloadedAfterCreate()
@@ -4303,10 +4315,10 @@ class VelociousDatabaseRecord {
4303
4315
 
4304
4316
  /**
4305
4317
  * Applies the database insert response to this record.
4306
- * @param {{data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string}} options - Inserted data, connection result, and primary key column name.
4318
+ * @param {{connection: import("../drivers/base.js").default, data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string}} options - Pinned insert connection, inserted data, connection result, and primary key column name.
4307
4319
  * @returns {Promise<void>} - Resolves when complete.
4308
4320
  */
4309
- async _applyInsertResult({data, insertResult, primaryKey}) {
4321
+ async _applyInsertResult({connection, data, insertResult, primaryKey}) {
4310
4322
  if (Array.isArray(insertResult) && insertResult[0] && insertResult[0][primaryKey]) {
4311
4323
  this._attributes = insertResult[0]
4312
4324
  this._changes = {}
@@ -4322,7 +4334,7 @@ class VelociousDatabaseRecord {
4322
4334
  return
4323
4335
  }
4324
4336
 
4325
- const id = await this._connection().lastInsertID()
4337
+ const id = await connection.lastInsertID()
4326
4338
 
4327
4339
  await this._reloadWithId(id)
4328
4340
  }