@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.
- package/CHANGELOG.md +122 -102
- package/README.md +728 -728
- package/package.json +59 -62
- package/skills/sdk-rn-local-db/SKILL.md +0 -1
- package/src/adapter/budgets.ts +67 -67
- package/src/adapter/collection.ts +303 -298
- package/src/adapter/deletion.ts +252 -252
- package/src/adapter/index.ts +64 -68
- package/src/adapter/lifecycle.ts +517 -520
- package/src/adapter/transaction.ts +71 -71
- package/src/artifacts/availability.ts +67 -67
- package/src/artifacts/cascade.ts +114 -114
- package/src/artifacts/paths.ts +29 -29
- package/src/artifacts/reconcile.ts +63 -63
- package/src/artifacts/store.ts +334 -345
- package/src/contract/adapter.ts +76 -78
- package/src/contract/attachment.ts +75 -78
- package/src/contract/backup.ts +28 -28
- package/src/contract/config.ts +54 -54
- package/src/contract/encryption.ts +34 -34
- package/src/contract/errors.ts +76 -76
- package/src/contract/migration.ts +42 -42
- package/src/contract/query.ts +81 -81
- package/src/contract/record.ts +46 -51
- package/src/contract/schema.ts +36 -36
- package/src/contract/scope.ts +36 -36
- package/src/contract/transaction.ts +52 -54
- package/src/crypto/chunked-cipher.ts +153 -150
- package/src/crypto/derive.ts +64 -64
- package/src/crypto/key-provider.ts +78 -78
- package/src/crypto/provider.ts +81 -81
- package/src/diagnostics/dev-log.ts +112 -108
- package/src/diagnostics/errors.ts +62 -66
- package/src/diagnostics/faults.ts +58 -64
- package/src/diagnostics/map-native.ts +108 -106
- package/src/diagnostics/redact.ts +137 -141
- package/src/index.ts +80 -110
- package/src/migration/executor.ts +132 -136
- package/src/migration/ledger.ts +95 -97
- package/src/migration/validate-chain.ts +65 -65
- package/src/platform/backup-android-verify.ts +88 -88
- package/src/platform/backup-ios.ts +48 -48
- package/src/platform/bytes.ts +144 -144
- package/src/platform/fs.ts +89 -89
- package/src/platform/location.ts +135 -135
- package/src/query/budgets.ts +54 -54
- package/src/query/compare.ts +91 -91
- package/src/query/cursor.ts +100 -100
- package/src/query/filter.ts +82 -82
- package/src/query/paginate.ts +51 -51
- package/src/query/sort.ts +55 -56
- package/src/query/validate.ts +171 -171
- package/src/schema/declare.ts +42 -45
- package/src/schema/validate-config.ts +158 -158
- package/src/scope/hash.ts +131 -134
- package/src/scope/normalize.ts +83 -90
- package/src/scope/validate.ts +83 -83
- package/src/sqlite/adapter.ts +26 -26
- package/src/sqlite/batch.ts +242 -242
- package/src/sqlite/catalog.ts +105 -108
- package/src/sqlite/connection.ts +227 -227
- package/src/sqlite/index-coverage.ts +54 -56
- package/src/sqlite/native.ts +83 -83
- package/src/sqlite/row-codec.ts +217 -221
- package/src/sqlite/sql-builder.ts +244 -244
- package/src/sqlite/transaction-queue.ts +87 -87
- package/src/sqlite/transactions.ts +33 -33
- package/src/sqlite/values.ts +24 -24
- package/src/testing/conformance/attachments.ts +138 -136
- package/src/testing/conformance/encryption.ts +150 -150
- package/src/testing/conformance/index.ts +161 -161
- package/src/testing/conformance/lifecycle.ts +99 -99
- package/src/testing/conformance/migrations.ts +177 -177
- package/src/testing/conformance/queries.ts +204 -167
- package/src/testing/conformance/records.ts +169 -169
- package/src/testing/conformance/scopes.ts +86 -88
- package/src/testing/conformance/transactions.ts +149 -149
- package/src/testing/faults.ts +21 -21
- package/src/testing/index.ts +17 -21
- package/src/testing/memory-adapter/collection.ts +214 -219
- package/src/testing/memory-adapter/index.ts +510 -492
- package/src/testing/memory-adapter/migrations.ts +117 -119
- package/src/testing/memory-adapter/reset.ts +50 -50
- 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (codePoint <
|
|
50
|
-
out.push(codePoint);
|
|
51
|
-
} else if (codePoint <
|
|
52
|
-
out.push(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
0x80 | (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (let i =
|
|
95
|
-
w[i]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
h[
|
|
124
|
-
h[
|
|
125
|
-
h[
|
|
126
|
-
h[
|
|
127
|
-
h[
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|