@chipmobilesdk/rn-local-db 0.2.0 → 0.2.1

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 +122 -102
  2. package/README.md +728 -728
  3. package/package.json +59 -62
  4. package/skills/sdk-rn-local-db/SKILL.md +0 -1
  5. package/src/adapter/budgets.ts +67 -67
  6. package/src/adapter/collection.ts +303 -298
  7. package/src/adapter/deletion.ts +252 -252
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -520
  10. package/src/adapter/transaction.ts +71 -71
  11. package/src/artifacts/availability.ts +67 -67
  12. package/src/artifacts/cascade.ts +114 -114
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -345
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -78
  18. package/src/contract/backup.ts +28 -28
  19. package/src/contract/config.ts +54 -54
  20. package/src/contract/encryption.ts +34 -34
  21. package/src/contract/errors.ts +76 -76
  22. package/src/contract/migration.ts +42 -42
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -51
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -54
  28. package/src/crypto/chunked-cipher.ts +153 -150
  29. package/src/crypto/derive.ts +64 -64
  30. package/src/crypto/key-provider.ts +78 -78
  31. package/src/crypto/provider.ts +81 -81
  32. package/src/diagnostics/dev-log.ts +112 -108
  33. package/src/diagnostics/errors.ts +62 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +137 -141
  37. package/src/index.ts +80 -110
  38. package/src/migration/executor.ts +132 -136
  39. package/src/migration/ledger.ts +95 -97
  40. package/src/migration/validate-chain.ts +65 -65
  41. package/src/platform/backup-android-verify.ts +88 -88
  42. package/src/platform/backup-ios.ts +48 -48
  43. package/src/platform/bytes.ts +144 -144
  44. package/src/platform/fs.ts +89 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -54
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -100
  49. package/src/query/filter.ts +82 -82
  50. package/src/query/paginate.ts +51 -51
  51. package/src/query/sort.ts +55 -56
  52. package/src/query/validate.ts +171 -171
  53. package/src/schema/declare.ts +42 -45
  54. package/src/schema/validate-config.ts +158 -158
  55. package/src/scope/hash.ts +131 -134
  56. package/src/scope/normalize.ts +83 -90
  57. package/src/scope/validate.ts +83 -83
  58. package/src/sqlite/adapter.ts +26 -26
  59. package/src/sqlite/batch.ts +242 -242
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -227
  62. package/src/sqlite/index-coverage.ts +54 -56
  63. package/src/sqlite/native.ts +83 -83
  64. package/src/sqlite/row-codec.ts +217 -221
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -87
  67. package/src/sqlite/transactions.ts +33 -33
  68. package/src/sqlite/values.ts +24 -24
  69. package/src/testing/conformance/attachments.ts +138 -136
  70. package/src/testing/conformance/encryption.ts +150 -150
  71. package/src/testing/conformance/index.ts +161 -161
  72. package/src/testing/conformance/lifecycle.ts +99 -99
  73. package/src/testing/conformance/migrations.ts +177 -177
  74. package/src/testing/conformance/queries.ts +204 -167
  75. package/src/testing/conformance/records.ts +169 -169
  76. package/src/testing/conformance/scopes.ts +86 -88
  77. package/src/testing/conformance/transactions.ts +149 -149
  78. package/src/testing/faults.ts +21 -21
  79. package/src/testing/index.ts +17 -21
  80. package/src/testing/memory-adapter/collection.ts +214 -219
  81. package/src/testing/memory-adapter/index.ts +510 -492
  82. package/src/testing/memory-adapter/migrations.ts +117 -119
  83. package/src/testing/memory-adapter/reset.ts +50 -50
  84. package/src/testing/memory-adapter/store.ts +128 -134
@@ -1,158 +1,158 @@
1
- /**
2
- * Configuration validation.
3
- *
4
- * Runs to completion before any write and before any migration executes
5
- * (FR-006, FR-053). Order matters: scope first, then collections, then the
6
- * migration chain — so a consumer fixes the outermost problem first rather
7
- * than chasing a downstream symptom.
8
- */
9
-
10
- import { Budgets, type DatabaseConfig } from '../contract/config';
11
- import { DiagnosticCodes } from '../contract/errors';
12
- import type { CollectionSchema } from '../contract/schema';
13
- import { raise } from '../diagnostics/errors';
14
- import { validateDescriptor } from '../scope/validate';
15
-
16
- const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]{0,62}$/;
17
-
18
- function configInvalid(message: string, detail?: Record<string, unknown>): never {
19
- raise(DiagnosticCodes.CONFIG_INVALID, { message, detail });
20
- }
21
-
22
- function validateCollection(collection: CollectionSchema, seen: Set<string>): void {
23
- if (!IDENTIFIER.test(collection.name)) {
24
- configInvalid('collection name is not a valid identifier', {
25
- collection: collection.name,
26
- });
27
- }
28
- if (seen.has(collection.name)) {
29
- configInvalid('duplicate collection name', { collection: collection.name });
30
- }
31
- seen.add(collection.name);
32
-
33
- const fieldNames = new Set<string>();
34
- for (const field of collection.fields ?? []) {
35
- if (!IDENTIFIER.test(field.name)) {
36
- configInvalid('field name is not a valid identifier', {
37
- collection: collection.name,
38
- });
39
- }
40
- if (fieldNames.has(field.name)) {
41
- configInvalid('duplicate field name', { collection: collection.name });
42
- }
43
- fieldNames.add(field.name);
44
- }
45
-
46
- const indexNames = new Set<string>();
47
- for (const index of collection.indexes ?? []) {
48
- if (!IDENTIFIER.test(index.name)) {
49
- configInvalid('index name is not a valid identifier', {
50
- collection: collection.name,
51
- });
52
- }
53
- if (indexNames.has(index.name)) {
54
- configInvalid('duplicate index name', { collection: collection.name });
55
- }
56
- indexNames.add(index.name);
57
- if (!Array.isArray(index.fields) || index.fields.length === 0) {
58
- configInvalid('index declares no fields', { collection: collection.name });
59
- }
60
- for (const field of index.fields) {
61
- if (!fieldNames.has(field)) {
62
- // The whole point of declaring fields is that the package can
63
- // tell a typo from a deliberate choice. Silently creating an
64
- // index over a column that will never exist would turn a config
65
- // bug into a mysterious full scan at runtime.
66
- configInvalid('index references an undeclared field', {
67
- collection: collection.name,
68
- });
69
- }
70
- }
71
- }
72
- }
73
-
74
- function validateMigrations(config: DatabaseConfig): void {
75
- const migrations = config.migrations ?? [];
76
- if (!Number.isInteger(config.schemaVersion) || config.schemaVersion < 1) {
77
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
78
- message: 'schemaVersion must be a positive integer',
79
- });
80
- }
81
-
82
- const versions = new Set<number>();
83
- for (const migration of migrations) {
84
- if (!Number.isInteger(migration.version) || migration.version < 1) {
85
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
86
- message: 'migration version must be a positive integer',
87
- });
88
- }
89
- if (versions.has(migration.version)) {
90
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
91
- message: 'duplicate migration version',
92
- detail: { version: migration.version },
93
- });
94
- }
95
- versions.add(migration.version);
96
- if (typeof migration.migrate !== 'function') {
97
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
98
- message: 'migration has no migrate function',
99
- detail: { version: migration.version },
100
- });
101
- }
102
- }
103
-
104
- // A chain must be able to carry a database from 1 to schemaVersion. A gap
105
- // means some upgrading user has no path across it, which is worse than
106
- // refusing to open.
107
- for (let version = 1; version <= config.schemaVersion; version += 1) {
108
- if (!versions.has(version)) {
109
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
110
- message: 'migration chain has a gap',
111
- detail: { version },
112
- });
113
- }
114
- }
115
-
116
- for (const version of versions) {
117
- if (version > config.schemaVersion) {
118
- raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
119
- message: 'migration version exceeds the declared schemaVersion',
120
- detail: { version },
121
- });
122
- }
123
- }
124
- }
125
-
126
- export function validateConfig(config: DatabaseConfig): void {
127
- if (!config || typeof config !== 'object') {
128
- configInvalid('database config is required');
129
- }
130
- if (typeof config.name !== 'string' || !IDENTIFIER.test(config.name)) {
131
- configInvalid('database name is not a valid identifier');
132
- }
133
-
134
- validateDescriptor(config.scope);
135
-
136
- if (!Array.isArray(config.collections) || config.collections.length === 0) {
137
- configInvalid('at least one collection must be declared');
138
- }
139
- if (config.collections.length > Budgets.MAX_COLLECTIONS_PER_DATABASE) {
140
- configInvalid('collection count exceeds the budget', {
141
- count: config.collections.length,
142
- limit: Budgets.MAX_COLLECTIONS_PER_DATABASE,
143
- });
144
- }
145
-
146
- const seen = new Set<string>();
147
- for (const collection of config.collections) {
148
- validateCollection(collection, seen);
149
- }
150
-
151
- validateMigrations(config);
152
-
153
- if (config.encryption && config.encryption.enabled) {
154
- if (typeof config.encryption.keyProvider?.getKey !== 'function') {
155
- configInvalid('encryption is enabled but no key provider was supplied');
156
- }
157
- }
158
- }
1
+ /**
2
+ * Configuration validation.
3
+ *
4
+ * Runs to completion before any write and before any migration executes
5
+ * (FR-006, FR-053). Order matters: scope first, then collections, then the
6
+ * migration chain — so a consumer fixes the outermost problem first rather
7
+ * than chasing a downstream symptom.
8
+ */
9
+
10
+ import { Budgets, type DatabaseConfig } from '../contract/config';
11
+ import { DiagnosticCodes } from '../contract/errors';
12
+ import type { CollectionSchema } from '../contract/schema';
13
+ import { raise } from '../diagnostics/errors';
14
+ import { validateDescriptor } from '../scope/validate';
15
+
16
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]{0,62}$/;
17
+
18
+ function configInvalid(message: string, detail?: Record<string, unknown>): never {
19
+ raise(DiagnosticCodes.CONFIG_INVALID, { message, detail });
20
+ }
21
+
22
+ function validateCollection(collection: CollectionSchema, seen: Set<string>): void {
23
+ if (!IDENTIFIER.test(collection.name)) {
24
+ configInvalid('collection name is not a valid identifier', {
25
+ collection: collection.name,
26
+ });
27
+ }
28
+ if (seen.has(collection.name)) {
29
+ configInvalid('duplicate collection name', { collection: collection.name });
30
+ }
31
+ seen.add(collection.name);
32
+
33
+ const fieldNames = new Set<string>();
34
+ for (const field of collection.fields ?? []) {
35
+ if (!IDENTIFIER.test(field.name)) {
36
+ configInvalid('field name is not a valid identifier', {
37
+ collection: collection.name,
38
+ });
39
+ }
40
+ if (fieldNames.has(field.name)) {
41
+ configInvalid('duplicate field name', { collection: collection.name });
42
+ }
43
+ fieldNames.add(field.name);
44
+ }
45
+
46
+ const indexNames = new Set<string>();
47
+ for (const index of collection.indexes ?? []) {
48
+ if (!IDENTIFIER.test(index.name)) {
49
+ configInvalid('index name is not a valid identifier', {
50
+ collection: collection.name,
51
+ });
52
+ }
53
+ if (indexNames.has(index.name)) {
54
+ configInvalid('duplicate index name', { collection: collection.name });
55
+ }
56
+ indexNames.add(index.name);
57
+ if (!Array.isArray(index.fields) || index.fields.length === 0) {
58
+ configInvalid('index declares no fields', { collection: collection.name });
59
+ }
60
+ for (const field of index.fields) {
61
+ if (!fieldNames.has(field)) {
62
+ // The whole point of declaring fields is that the package can
63
+ // tell a typo from a deliberate choice. Silently creating an
64
+ // index over a column that will never exist would turn a config
65
+ // bug into a mysterious full scan at runtime.
66
+ configInvalid('index references an undeclared field', {
67
+ collection: collection.name,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ function validateMigrations(config: DatabaseConfig): void {
75
+ const migrations = config.migrations ?? [];
76
+ if (!Number.isInteger(config.schemaVersion) || config.schemaVersion < 1) {
77
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
78
+ message: 'schemaVersion must be a positive integer',
79
+ });
80
+ }
81
+
82
+ const versions = new Set<number>();
83
+ for (const migration of migrations) {
84
+ if (!Number.isInteger(migration.version) || migration.version < 1) {
85
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
86
+ message: 'migration version must be a positive integer',
87
+ });
88
+ }
89
+ if (versions.has(migration.version)) {
90
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
91
+ message: 'duplicate migration version',
92
+ detail: { version: migration.version },
93
+ });
94
+ }
95
+ versions.add(migration.version);
96
+ if (typeof migration.migrate !== 'function') {
97
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
98
+ message: 'migration has no migrate function',
99
+ detail: { version: migration.version },
100
+ });
101
+ }
102
+ }
103
+
104
+ // A chain must be able to carry a database from 1 to schemaVersion. A gap
105
+ // means some upgrading user has no path across it, which is worse than
106
+ // refusing to open.
107
+ for (let version = 1; version <= config.schemaVersion; version += 1) {
108
+ if (!versions.has(version)) {
109
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
110
+ message: 'migration chain has a gap',
111
+ detail: { version },
112
+ });
113
+ }
114
+ }
115
+
116
+ for (const version of versions) {
117
+ if (version > config.schemaVersion) {
118
+ raise(DiagnosticCodes.MIGRATION_CONFIG_INVALID, {
119
+ message: 'migration version exceeds the declared schemaVersion',
120
+ detail: { version },
121
+ });
122
+ }
123
+ }
124
+ }
125
+
126
+ export function validateConfig(config: DatabaseConfig): void {
127
+ if (!config || typeof config !== 'object') {
128
+ configInvalid('database config is required');
129
+ }
130
+ if (typeof config.name !== 'string' || !IDENTIFIER.test(config.name)) {
131
+ configInvalid('database name is not a valid identifier');
132
+ }
133
+
134
+ validateDescriptor(config.scope);
135
+
136
+ if (!Array.isArray(config.collections) || config.collections.length === 0) {
137
+ configInvalid('at least one collection must be declared');
138
+ }
139
+ if (config.collections.length > Budgets.MAX_COLLECTIONS_PER_DATABASE) {
140
+ configInvalid('collection count exceeds the budget', {
141
+ count: config.collections.length,
142
+ limit: Budgets.MAX_COLLECTIONS_PER_DATABASE,
143
+ });
144
+ }
145
+
146
+ const seen = new Set<string>();
147
+ for (const collection of config.collections) {
148
+ validateCollection(collection, seen);
149
+ }
150
+
151
+ validateMigrations(config);
152
+
153
+ if (config.encryption && config.encryption.enabled) {
154
+ if (typeof config.encryption.keyProvider?.getKey !== 'function') {
155
+ configInvalid('encryption is enabled but no key provider was supplied');
156
+ }
157
+ }
158
+ }
package/src/scope/hash.ts CHANGED
@@ -1,134 +1,131 @@
1
- /**
2
- * Scope key derivation.
3
- *
4
- * The directory a scope lives in is named by the SHA-256 digest of its
5
- * canonical descriptor, never by the components themselves. FR-014 requires
6
- * scope components be treated as sensitive, and a directory named after an
7
- * email address puts that address in plaintext on disk — visible even when
8
- * the database itself is encrypted, and carried into any crash report that
9
- * includes a file path.
10
- *
11
- * Hashing also removes filesystem charset and length limits from the problem,
12
- * which is what lets validation reject rather than sanitize (research.md R2).
13
- *
14
- * Implemented in pure TypeScript rather than over a native crypto peer so the
15
- * scope layer stays importable from anywhere in the package, including the
16
- * in-memory adapter running under Jest.
17
- *
18
- * Verified against Node's own SHA-256 in scope-resolution.test.ts — a wrong
19
- * digest would silently merge or split scopes, so it is checked against a
20
- * reference implementation rather than against a snapshot of this one.
21
- */
22
-
23
- /* eslint-disable no-bitwise -- SHA-256 is defined in terms of bitwise operations. */
24
-
25
- const K = new Uint32Array([
26
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
27
- 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
28
- 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
29
- 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
30
- 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
31
- 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
32
- 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
33
- 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
34
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
35
- 0xc67178f2,
36
- ]);
37
-
38
- function rotr(value: number, bits: number): number {
39
- return (value >>> bits) | (value << (32 - bits));
40
- }
41
-
42
- function utf8Bytes(input: string): Uint8Array {
43
- const out: number[] = [];
44
- for (let i = 0; i < input.length; i += 1) {
45
- let codePoint = input.codePointAt(i)!;
46
- if (codePoint > 0xffff) {
47
- i += 1;
48
- }
49
- if (codePoint < 0x80) {
50
- out.push(codePoint);
51
- } else if (codePoint < 0x800) {
52
- out.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
53
- } else if (codePoint < 0x10000) {
54
- out.push(
55
- 0xe0 | (codePoint >> 12),
56
- 0x80 | ((codePoint >> 6) & 0x3f),
57
- 0x80 | (codePoint & 0x3f),
58
- );
59
- } else {
60
- out.push(
61
- 0xf0 | (codePoint >> 18),
62
- 0x80 | ((codePoint >> 12) & 0x3f),
63
- 0x80 | ((codePoint >> 6) & 0x3f),
64
- 0x80 | (codePoint & 0x3f),
65
- );
66
- }
67
- }
68
- return Uint8Array.from(out);
69
- }
70
-
71
- export function utf8ByteLength(input: string): number {
72
- return utf8Bytes(input).length;
73
- }
74
-
75
- export function sha256Hex(input: string): string {
76
- const bytes = utf8Bytes(input);
77
- const bitLength = bytes.length * 8;
78
- const paddedLength = (((bytes.length + 9) >> 6) + 1) << 6;
79
- const padded = new Uint8Array(paddedLength);
80
- padded.set(bytes);
81
- padded[bytes.length] = 0x80;
82
-
83
- const view = new DataView(padded.buffer);
84
- view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
85
- view.setUint32(paddedLength - 4, bitLength >>> 0, false);
86
-
87
- const h = new Uint32Array([
88
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
89
- 0x5be0cd19,
90
- ]);
91
- const w = new Uint32Array(64);
92
-
93
- for (let offset = 0; offset < paddedLength; offset += 64) {
94
- for (let i = 0; i < 16; i += 1) {
95
- w[i] = view.getUint32(offset + i * 4, false);
96
- }
97
- for (let i = 16; i < 64; i += 1) {
98
- const s0 = rotr(w[i - 15]!, 7) ^ rotr(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3);
99
- const s1 = rotr(w[i - 2]!, 17) ^ rotr(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10);
100
- w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
101
- }
102
-
103
- let [a, b, c, d, e, f, g, hh] = [h[0]!, h[1]!, h[2]!, h[3]!, h[4]!, h[5]!, h[6]!, h[7]!];
104
-
105
- for (let i = 0; i < 64; i += 1) {
106
- const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
107
- const ch = (e & f) ^ (~e & g);
108
- const temp1 = (hh + S1 + ch + K[i]! + w[i]!) >>> 0;
109
- const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
110
- const maj = (a & b) ^ (a & c) ^ (b & c);
111
- const temp2 = (S0 + maj) >>> 0;
112
-
113
- hh = g;
114
- g = f;
115
- f = e;
116
- e = (d + temp1) >>> 0;
117
- d = c;
118
- c = b;
119
- b = a;
120
- a = (temp1 + temp2) >>> 0;
121
- }
122
-
123
- h[0] = (h[0]! + a) >>> 0;
124
- h[1] = (h[1]! + b) >>> 0;
125
- h[2] = (h[2]! + c) >>> 0;
126
- h[3] = (h[3]! + d) >>> 0;
127
- h[4] = (h[4]! + e) >>> 0;
128
- h[5] = (h[5]! + f) >>> 0;
129
- h[6] = (h[6]! + g) >>> 0;
130
- h[7] = (h[7]! + hh) >>> 0;
131
- }
132
-
133
- return Array.from(h, value => value.toString(16).padStart(8, '0')).join('');
134
- }
1
+ /**
2
+ * Scope key derivation.
3
+ *
4
+ * The directory a scope lives in is named by the SHA-256 digest of its
5
+ * canonical descriptor, never by the components themselves. FR-014 requires
6
+ * scope components be treated as sensitive, and a directory named after an
7
+ * email address puts that address in plaintext on disk — visible even when
8
+ * the database itself is encrypted, and carried into any crash report that
9
+ * includes a file path.
10
+ *
11
+ * Hashing also removes filesystem charset and length limits from the problem,
12
+ * which is what lets validation reject rather than sanitize (research.md R2).
13
+ *
14
+ * Implemented in pure TypeScript rather than over a native crypto peer so the
15
+ * scope layer stays importable from anywhere in the package, including the
16
+ * in-memory adapter running under Jest.
17
+ *
18
+ * Verified against Node's own SHA-256 in scope-resolution.test.ts — a wrong
19
+ * digest would silently merge or split scopes, so it is checked against a
20
+ * reference implementation rather than against a snapshot of this one.
21
+ */
22
+
23
+ /* eslint-disable no-bitwise -- SHA-256 is defined in terms of bitwise operations. */
24
+
25
+ const K = new Uint32Array([
26
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
27
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
28
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
29
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
30
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
31
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
32
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
33
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
34
+ ]);
35
+
36
+ function rotr(value: number, bits: number): number {
37
+ return (value >>> bits) | (value << (32 - bits));
38
+ }
39
+
40
+ function utf8Bytes(input: string): Uint8Array {
41
+ const out: number[] = [];
42
+ for (let i = 0; i < input.length; i += 1) {
43
+ let codePoint = input.codePointAt(i)!;
44
+ if (codePoint > 0xffff) {
45
+ i += 1;
46
+ }
47
+ if (codePoint < 0x80) {
48
+ out.push(codePoint);
49
+ } else if (codePoint < 0x800) {
50
+ out.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
51
+ } else if (codePoint < 0x10000) {
52
+ out.push(
53
+ 0xe0 | (codePoint >> 12),
54
+ 0x80 | ((codePoint >> 6) & 0x3f),
55
+ 0x80 | (codePoint & 0x3f),
56
+ );
57
+ } else {
58
+ out.push(
59
+ 0xf0 | (codePoint >> 18),
60
+ 0x80 | ((codePoint >> 12) & 0x3f),
61
+ 0x80 | ((codePoint >> 6) & 0x3f),
62
+ 0x80 | (codePoint & 0x3f),
63
+ );
64
+ }
65
+ }
66
+ return Uint8Array.from(out);
67
+ }
68
+
69
+ export function utf8ByteLength(input: string): number {
70
+ return utf8Bytes(input).length;
71
+ }
72
+
73
+ export function sha256Hex(input: string): string {
74
+ const bytes = utf8Bytes(input);
75
+ const bitLength = bytes.length * 8;
76
+ const paddedLength = (((bytes.length + 9) >> 6) + 1) << 6;
77
+ const padded = new Uint8Array(paddedLength);
78
+ padded.set(bytes);
79
+ padded[bytes.length] = 0x80;
80
+
81
+ const view = new DataView(padded.buffer);
82
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
83
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
84
+
85
+ const h = new Uint32Array([
86
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
87
+ ]);
88
+ const w = new Uint32Array(64);
89
+
90
+ for (let offset = 0; offset < paddedLength; offset += 64) {
91
+ for (let i = 0; i < 16; i += 1) {
92
+ w[i] = view.getUint32(offset + i * 4, false);
93
+ }
94
+ for (let i = 16; i < 64; i += 1) {
95
+ const s0 = rotr(w[i - 15]!, 7) ^ rotr(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3);
96
+ const s1 = rotr(w[i - 2]!, 17) ^ rotr(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10);
97
+ w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
98
+ }
99
+
100
+ let [a, b, c, d, e, f, g, hh] = [h[0]!, h[1]!, h[2]!, h[3]!, h[4]!, h[5]!, h[6]!, h[7]!];
101
+
102
+ for (let i = 0; i < 64; i += 1) {
103
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
104
+ const ch = (e & f) ^ (~e & g);
105
+ const temp1 = (hh + S1 + ch + K[i]! + w[i]!) >>> 0;
106
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
107
+ const maj = (a & b) ^ (a & c) ^ (b & c);
108
+ const temp2 = (S0 + maj) >>> 0;
109
+
110
+ hh = g;
111
+ g = f;
112
+ f = e;
113
+ e = (d + temp1) >>> 0;
114
+ d = c;
115
+ c = b;
116
+ b = a;
117
+ a = (temp1 + temp2) >>> 0;
118
+ }
119
+
120
+ h[0] = (h[0]! + a) >>> 0;
121
+ h[1] = (h[1]! + b) >>> 0;
122
+ h[2] = (h[2]! + c) >>> 0;
123
+ h[3] = (h[3]! + d) >>> 0;
124
+ h[4] = (h[4]! + e) >>> 0;
125
+ h[5] = (h[5]! + f) >>> 0;
126
+ h[6] = (h[6]! + g) >>> 0;
127
+ h[7] = (h[7]! + hh) >>> 0;
128
+ }
129
+
130
+ return Array.from(h, value => value.toString(16).padStart(8, '0')).join('');
131
+ }