cursedbelt-core 2.5.0 → 2.7.0
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/dist/core/file-tree/fileTreeModel.d.ts +18 -5
- package/dist/core/file-tree/fileTreeModel.js +11 -9
- package/dist/core/passwordPolicy.d.ts +9 -7
- package/dist/core/passwordPolicy.js +9 -7
- package/package.json +1 -1
- package/src/core/file-tree/fileTreeModel.spec.ts +24 -7
- package/src/core/file-tree/fileTreeModel.ts +30 -11
- package/src/core/passwordPolicy.spec.ts +3 -3
- package/src/core/passwordPolicy.ts +9 -7
|
@@ -101,10 +101,12 @@ export interface FileTreeNode {
|
|
|
101
101
|
* container of its own — a drop on the middle half of its row is `{ kind: 'into',
|
|
102
102
|
* parentId: <the file> }`. `apps/desk` is why: a workbook is a leaf in its tree, and
|
|
103
103
|
* dropping a sheet onto one makes the sheet one of its tabs (the owner, desk file `02`).
|
|
104
|
-
* The edges of the row
|
|
105
|
-
*
|
|
106
|
-
* wall of rows that refuse everything that cannot go inside them.
|
|
107
|
-
*
|
|
104
|
+
* The edges of the row still mean *the folder the file is in*, and so does the middle for
|
|
105
|
+
* any drag {@link ResolveDropOptions.takesDrop} says is not a candidate — so a column of
|
|
106
|
+
* such files is never a wall of rows that refuse everything that cannot go inside them.
|
|
107
|
+
* A candidate that {@link ResolveDropOptions.canDrop} vetoes is REFUSED with its words:
|
|
108
|
+
* the file was the target, and why it cannot take this one has a remedy worth saying.
|
|
109
|
+
* Absent or `false`, a file row means its folder along its whole height, as before.
|
|
108
110
|
*/
|
|
109
111
|
acceptsDrop?: boolean;
|
|
110
112
|
/**
|
|
@@ -233,6 +235,17 @@ export interface ResolveDropOptions {
|
|
|
233
235
|
* this tree is that an impossible drop is never drawn as possible.
|
|
234
236
|
*/
|
|
235
237
|
canDrop?: (dragId: string | null, target: FileTreeDrop) => string | null;
|
|
238
|
+
/**
|
|
239
|
+
* Whether a file that accepts drops (`acceptsDrop: true` on a FILE) is a candidate for
|
|
240
|
+
* THIS drag at all. Default: every drag is.
|
|
241
|
+
*
|
|
242
|
+
* 🔴 This is the question {@link canDrop} cannot answer, because its `null` means "allow":
|
|
243
|
+
* `apps/desk` takes a SHEET into a workbook and never a note, so a note over a workbook
|
|
244
|
+
* must mean the workbook's folder — not a refusal, and not a merge. `false` here is that:
|
|
245
|
+
* the row goes back to meaning its folder. `true` makes the file the target, and then
|
|
246
|
+
* `canDrop` decides, in words — *"“Beta” has 2 sheets"* is a refusal with a remedy.
|
|
247
|
+
*/
|
|
248
|
+
takesDrop?: (dragId: string | null, file: FileTreeNode) => boolean;
|
|
236
249
|
}
|
|
237
250
|
/**
|
|
238
251
|
* Where the pointer currently is, as a decision.
|
|
@@ -241,7 +254,7 @@ export interface ResolveDropOptions {
|
|
|
241
254
|
* offered.** Every refusal below returns a reason rather than a null target, so the cursor
|
|
242
255
|
* and the chip can say no while the row is still in the air.
|
|
243
256
|
*/
|
|
244
|
-
export declare function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder, lockedReason, canDrop, }: ResolveDropOptions): DropOffer;
|
|
257
|
+
export declare function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder, lockedReason, canDrop, takesDrop, }: ResolveDropOptions): DropOffer;
|
|
245
258
|
export interface KeyboardMoveOptions {
|
|
246
259
|
nodes: readonly FileTreeNode[];
|
|
247
260
|
/** The rows on screen — siblings are read off these, which is every sibling by construction. */
|
|
@@ -125,7 +125,7 @@ export const INTO_ZONE = 0.5;
|
|
|
125
125
|
* offered.** Every refusal below returns a reason rather than a null target, so the cursor
|
|
126
126
|
* and the chip can say no while the row is still in the air.
|
|
127
127
|
*/
|
|
128
|
-
export function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder = false, lockedReason = 'That folder is locked', canDrop, }) {
|
|
128
|
+
export function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder = false, lockedReason = 'That folder is locked', canDrop, takesDrop, }) {
|
|
129
129
|
// Blank space under the last row: out of every folder. The one gesture that needs no
|
|
130
130
|
// target, and the only way to say "top level" when the top level has no row of its own.
|
|
131
131
|
if (!row)
|
|
@@ -156,15 +156,17 @@ export function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder = fa
|
|
|
156
156
|
return { target: null, refused: true, reason: lockedReason };
|
|
157
157
|
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
158
158
|
};
|
|
159
|
-
// A FILE the consumer opted in as a container takes
|
|
160
|
-
// see `FileTreeNode.acceptsDrop
|
|
161
|
-
|
|
159
|
+
// A FILE the consumer opted in as a container takes a CANDIDATE drag on the middle of its
|
|
160
|
+
// row, vetted with words — see `FileTreeNode.acceptsDrop` and `takesDrop`. Anything else
|
|
161
|
+
// falls through to meaning its folder below.
|
|
162
|
+
if (target.kind === 'file' &&
|
|
163
|
+
!allowReorder &&
|
|
164
|
+
target.acceptsDrop === true &&
|
|
165
|
+
target.id !== dragId &&
|
|
166
|
+
(takesDrop?.(dragId, target) ?? true)) {
|
|
162
167
|
const band = (1 - INTO_ZONE) / 2;
|
|
163
|
-
if (offsetRatio > band && offsetRatio < 1 - band)
|
|
164
|
-
|
|
165
|
-
if (into.target)
|
|
166
|
-
return into;
|
|
167
|
-
}
|
|
168
|
+
if (offsetRatio > band && offsetRatio < 1 - band)
|
|
169
|
+
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
168
170
|
}
|
|
169
171
|
// A FILE row means "the folder that row is in" — what every file explorer does, and what
|
|
170
172
|
// keeps a long list of files from being a wall of rows nothing can be dropped on.
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place a satellite's owner-password floor is decided.
|
|
3
3
|
*
|
|
4
|
-
* ── Why this is shared, and why the default is
|
|
4
|
+
* ── Why this is shared, and why the default is 6 ────────────────────────────
|
|
5
5
|
* Three apps had independently written a `MIN_PASSWORD_BYTES` constant into
|
|
6
6
|
* their own `scripts/owner.ts`, and they had already drifted (9, 14, 14) with
|
|
7
7
|
* a paragraph in each explaining itself against the others. A floor is a
|
|
8
8
|
* POLICY, and a policy copied three times is three policies.
|
|
9
9
|
*
|
|
10
|
-
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} =
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} = 6 — the owner, 2026-09-24:
|
|
11
|
+
* *"make the minimum password length 6 characters"* (it was 9). It is a SIGN-IN
|
|
12
|
+
* floor: every door that uses it rate-limits guesses, so a short password costs
|
|
13
|
+
* an attacker attempts, not seconds. A secret that can be guessed OFFLINE (vault's
|
|
14
|
+
* passphrase, which derives its encryption key) raises it at its own call site.
|
|
13
15
|
* Argon2id — what every caller hashes with — is what actually carries the cost
|
|
14
16
|
* of a guess here; these apps have exactly ONE principal and no enumerable user
|
|
15
17
|
* list, so the marginal value of characters 10–14 is small and the friction is
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* {@link resolveMinPasswordBytes} takes an optional override and **refuses to
|
|
26
28
|
* go below the default**. An app that genuinely runs at higher stakes says so
|
|
27
29
|
* at its own call site, in one visible argument, with a comment saying why.
|
|
28
|
-
*
|
|
30
|
+
* `vault` does (9, since 2026-09-24 — its passphrase is an encryption key). The retired orch companion app did (14, as the single
|
|
29
31
|
* internet-facing door into the host that runs `claude` in every repo) until
|
|
30
32
|
* the owner dropped it to the shared floor on 2026-07-26.
|
|
31
33
|
*
|
|
@@ -38,12 +40,12 @@
|
|
|
38
40
|
* they read, so a multi-byte character counts as more than one, which is the
|
|
39
41
|
* conservative direction).
|
|
40
42
|
*/
|
|
41
|
-
export declare const DEFAULT_MIN_PASSWORD_BYTES =
|
|
43
|
+
export declare const DEFAULT_MIN_PASSWORD_BYTES = 6;
|
|
42
44
|
/**
|
|
43
45
|
* The floor this app enforces: the default, or a higher one it asks for.
|
|
44
46
|
*
|
|
45
47
|
* Throws on an override below the default rather than silently clamping. A
|
|
46
|
-
* caller passing
|
|
48
|
+
* caller passing 4 believes it is enforcing 4; being handed 6 without a word is
|
|
47
49
|
* how a security floor becomes folklore.
|
|
48
50
|
*/
|
|
49
51
|
export declare function resolveMinPasswordBytes(override?: number): number;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place a satellite's owner-password floor is decided.
|
|
3
3
|
*
|
|
4
|
-
* ── Why this is shared, and why the default is
|
|
4
|
+
* ── Why this is shared, and why the default is 6 ────────────────────────────
|
|
5
5
|
* Three apps had independently written a `MIN_PASSWORD_BYTES` constant into
|
|
6
6
|
* their own `scripts/owner.ts`, and they had already drifted (9, 14, 14) with
|
|
7
7
|
* a paragraph in each explaining itself against the others. A floor is a
|
|
8
8
|
* POLICY, and a policy copied three times is three policies.
|
|
9
9
|
*
|
|
10
|
-
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} =
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} = 6 — the owner, 2026-09-24:
|
|
11
|
+
* *"make the minimum password length 6 characters"* (it was 9). It is a SIGN-IN
|
|
12
|
+
* floor: every door that uses it rate-limits guesses, so a short password costs
|
|
13
|
+
* an attacker attempts, not seconds. A secret that can be guessed OFFLINE (vault's
|
|
14
|
+
* passphrase, which derives its encryption key) raises it at its own call site.
|
|
13
15
|
* Argon2id — what every caller hashes with — is what actually carries the cost
|
|
14
16
|
* of a guess here; these apps have exactly ONE principal and no enumerable user
|
|
15
17
|
* list, so the marginal value of characters 10–14 is small and the friction is
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* {@link resolveMinPasswordBytes} takes an optional override and **refuses to
|
|
26
28
|
* go below the default**. An app that genuinely runs at higher stakes says so
|
|
27
29
|
* at its own call site, in one visible argument, with a comment saying why.
|
|
28
|
-
*
|
|
30
|
+
* `vault` does (9, since 2026-09-24 — its passphrase is an encryption key). The retired orch companion app did (14, as the single
|
|
29
31
|
* internet-facing door into the host that runs `claude` in every repo) until
|
|
30
32
|
* the owner dropped it to the shared floor on 2026-07-26.
|
|
31
33
|
*
|
|
@@ -38,12 +40,12 @@
|
|
|
38
40
|
* they read, so a multi-byte character counts as more than one, which is the
|
|
39
41
|
* conservative direction).
|
|
40
42
|
*/
|
|
41
|
-
export const DEFAULT_MIN_PASSWORD_BYTES =
|
|
43
|
+
export const DEFAULT_MIN_PASSWORD_BYTES = 6;
|
|
42
44
|
/**
|
|
43
45
|
* The floor this app enforces: the default, or a higher one it asks for.
|
|
44
46
|
*
|
|
45
47
|
* Throws on an override below the default rather than silently clamping. A
|
|
46
|
-
* caller passing
|
|
48
|
+
* caller passing 4 believes it is enforcing 4; being handed 6 without a word is
|
|
47
49
|
* how a security floor becomes folklore.
|
|
48
50
|
*/
|
|
49
51
|
export function resolveMinPasswordBytes(override) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
|
|
@@ -246,8 +246,14 @@ describe('resolveDrop onto a file that accepts drops', () => {
|
|
|
246
246
|
{ id: 'loose', name: 'loose.csv', kind: 'file' },
|
|
247
247
|
];
|
|
248
248
|
const rows = flattenFileTree(nodes, []);
|
|
249
|
-
const on = (
|
|
250
|
-
|
|
249
|
+
const on = (
|
|
250
|
+
id: string,
|
|
251
|
+
dragId: string,
|
|
252
|
+
offsetRatio: number,
|
|
253
|
+
canDrop?: (d: string | null, t: FileTreeDrop) => string | null,
|
|
254
|
+
takesDrop?: (d: string | null, file: FileTreeNode) => boolean,
|
|
255
|
+
) =>
|
|
256
|
+
resolveDrop({ nodes, row: rowFor(rows, id), offsetRatio, dragId, ...(canDrop ? { canDrop } : {}), ...(takesDrop ? { takesDrop } : {}) });
|
|
251
257
|
|
|
252
258
|
test('the middle of its row is INTO the file', () => {
|
|
253
259
|
expect(on('q3', 'loose', 0.5).target).toEqual({ kind: 'into', parentId: 'q3' });
|
|
@@ -258,11 +264,22 @@ describe('resolveDrop onto a file that accepts drops', () => {
|
|
|
258
264
|
expect(on('q3', 'loose', 0.9).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
259
265
|
});
|
|
260
266
|
|
|
261
|
-
test('🔴 a
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(on('q3', 'report', 0.5, onlySheets).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
265
|
-
expect(on('q3', 'loose', 0.5, onlySheets).target).toEqual({ kind: 'into', parentId: 'q3' });
|
|
267
|
+
test('🔴 a drag that is not a candidate means the folder — not a refusal, not the file', () => {
|
|
268
|
+
// desk: a note over a workbook files it beside the workbook.
|
|
269
|
+
const onlySheets = (dragId: string | null) => dragId === 'loose';
|
|
270
|
+
expect(on('q3', 'report', 0.5, undefined, onlySheets).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
271
|
+
expect(on('q3', 'loose', 0.5, undefined, onlySheets).target).toEqual({ kind: 'into', parentId: 'q3' });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('🔴 a candidate the consumer vetoes is REFUSED, in its words — the file was the target', () => {
|
|
275
|
+
const full = (_dragId: string | null, target: FileTreeDrop) =>
|
|
276
|
+
target.kind === 'into' && target.parentId === 'q3' ? '“loose” has 2 sheets' : null;
|
|
277
|
+
const offer = on('q3', 'loose', 0.5, full);
|
|
278
|
+
expect(offer.target).toBeNull();
|
|
279
|
+
expect(offer.refused).toBe(true);
|
|
280
|
+
expect(offer.reason).toBe('“loose” has 2 sheets');
|
|
281
|
+
// …and the edges still file it beside the workbook.
|
|
282
|
+
expect(on('q3', 'loose', 0.1, full).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
266
283
|
});
|
|
267
284
|
|
|
268
285
|
test('a file never takes itself', () => {
|
|
@@ -104,10 +104,12 @@ export interface FileTreeNode {
|
|
|
104
104
|
* container of its own — a drop on the middle half of its row is `{ kind: 'into',
|
|
105
105
|
* parentId: <the file> }`. `apps/desk` is why: a workbook is a leaf in its tree, and
|
|
106
106
|
* dropping a sheet onto one makes the sheet one of its tabs (the owner, desk file `02`).
|
|
107
|
-
* The edges of the row
|
|
108
|
-
*
|
|
109
|
-
* wall of rows that refuse everything that cannot go inside them.
|
|
110
|
-
*
|
|
107
|
+
* The edges of the row still mean *the folder the file is in*, and so does the middle for
|
|
108
|
+
* any drag {@link ResolveDropOptions.takesDrop} says is not a candidate — so a column of
|
|
109
|
+
* such files is never a wall of rows that refuse everything that cannot go inside them.
|
|
110
|
+
* A candidate that {@link ResolveDropOptions.canDrop} vetoes is REFUSED with its words:
|
|
111
|
+
* the file was the target, and why it cannot take this one has a remedy worth saying.
|
|
112
|
+
* Absent or `false`, a file row means its folder along its whole height, as before.
|
|
111
113
|
*/
|
|
112
114
|
acceptsDrop?: boolean;
|
|
113
115
|
/**
|
|
@@ -311,6 +313,17 @@ export interface ResolveDropOptions {
|
|
|
311
313
|
* this tree is that an impossible drop is never drawn as possible.
|
|
312
314
|
*/
|
|
313
315
|
canDrop?: (dragId: string | null, target: FileTreeDrop) => string | null;
|
|
316
|
+
/**
|
|
317
|
+
* Whether a file that accepts drops (`acceptsDrop: true` on a FILE) is a candidate for
|
|
318
|
+
* THIS drag at all. Default: every drag is.
|
|
319
|
+
*
|
|
320
|
+
* 🔴 This is the question {@link canDrop} cannot answer, because its `null` means "allow":
|
|
321
|
+
* `apps/desk` takes a SHEET into a workbook and never a note, so a note over a workbook
|
|
322
|
+
* must mean the workbook's folder — not a refusal, and not a merge. `false` here is that:
|
|
323
|
+
* the row goes back to meaning its folder. `true` makes the file the target, and then
|
|
324
|
+
* `canDrop` decides, in words — *"“Beta” has 2 sheets"* is a refusal with a remedy.
|
|
325
|
+
*/
|
|
326
|
+
takesDrop?: (dragId: string | null, file: FileTreeNode) => boolean;
|
|
314
327
|
}
|
|
315
328
|
|
|
316
329
|
/**
|
|
@@ -328,6 +341,7 @@ export function resolveDrop({
|
|
|
328
341
|
allowReorder = false,
|
|
329
342
|
lockedReason = 'That folder is locked',
|
|
330
343
|
canDrop,
|
|
344
|
+
takesDrop,
|
|
331
345
|
}: ResolveDropOptions): DropOffer {
|
|
332
346
|
// Blank space under the last row: out of every folder. The one gesture that needs no
|
|
333
347
|
// target, and the only way to say "top level" when the top level has no row of its own.
|
|
@@ -362,14 +376,19 @@ export function resolveDrop({
|
|
|
362
376
|
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
363
377
|
};
|
|
364
378
|
|
|
365
|
-
// A FILE the consumer opted in as a container takes
|
|
366
|
-
// see `FileTreeNode.acceptsDrop
|
|
367
|
-
|
|
379
|
+
// A FILE the consumer opted in as a container takes a CANDIDATE drag on the middle of its
|
|
380
|
+
// row, vetted with words — see `FileTreeNode.acceptsDrop` and `takesDrop`. Anything else
|
|
381
|
+
// falls through to meaning its folder below.
|
|
382
|
+
if (
|
|
383
|
+
target.kind === 'file' &&
|
|
384
|
+
!allowReorder &&
|
|
385
|
+
target.acceptsDrop === true &&
|
|
386
|
+
target.id !== dragId &&
|
|
387
|
+
(takesDrop?.(dragId, target) ?? true)
|
|
388
|
+
) {
|
|
368
389
|
const band = (1 - INTO_ZONE) / 2;
|
|
369
|
-
if (offsetRatio > band && offsetRatio < 1 - band)
|
|
370
|
-
|
|
371
|
-
if (into.target) return into;
|
|
372
|
-
}
|
|
390
|
+
if (offsetRatio > band && offsetRatio < 1 - band)
|
|
391
|
+
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
373
392
|
}
|
|
374
393
|
|
|
375
394
|
// A FILE row means "the folder that row is in" — what every file explorer does, and what
|
|
@@ -7,13 +7,13 @@ import { DEFAULT_MIN_PASSWORD_BYTES, resolveMinPasswordBytes } from './passwordP
|
|
|
7
7
|
describe('resolveMinPasswordBytes', () => {
|
|
8
8
|
it('is the shared default when an app asks for nothing', () => {
|
|
9
9
|
expect(resolveMinPasswordBytes()).toBe(DEFAULT_MIN_PASSWORD_BYTES);
|
|
10
|
-
expect(DEFAULT_MIN_PASSWORD_BYTES).toBe(
|
|
10
|
+
expect(DEFAULT_MIN_PASSWORD_BYTES).toBe(6);
|
|
11
11
|
});
|
|
12
12
|
it('lets an app raise it, visibly, at its own call site', () => {
|
|
13
13
|
expect(resolveMinPasswordBytes(14)).toBe(14);
|
|
14
14
|
});
|
|
15
15
|
it('🔴 refuses to go BELOW the default, and refuses a non-integer', () => {
|
|
16
|
-
expect(() => resolveMinPasswordBytes(
|
|
17
|
-
expect(() => resolveMinPasswordBytes(
|
|
16
|
+
expect(() => resolveMinPasswordBytes(5)).toThrow(/may only be RAISED/);
|
|
17
|
+
expect(() => resolveMinPasswordBytes(6.5)).toThrow(/must be an integer/);
|
|
18
18
|
});
|
|
19
19
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place a satellite's owner-password floor is decided.
|
|
3
3
|
*
|
|
4
|
-
* ── Why this is shared, and why the default is
|
|
4
|
+
* ── Why this is shared, and why the default is 6 ────────────────────────────
|
|
5
5
|
* Three apps had independently written a `MIN_PASSWORD_BYTES` constant into
|
|
6
6
|
* their own `scripts/owner.ts`, and they had already drifted (9, 14, 14) with
|
|
7
7
|
* a paragraph in each explaining itself against the others. A floor is a
|
|
8
8
|
* POLICY, and a policy copied three times is three policies.
|
|
9
9
|
*
|
|
10
|
-
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} =
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The default is {@link DEFAULT_MIN_PASSWORD_BYTES} = 6 — the owner, 2026-09-24:
|
|
11
|
+
* *"make the minimum password length 6 characters"* (it was 9). It is a SIGN-IN
|
|
12
|
+
* floor: every door that uses it rate-limits guesses, so a short password costs
|
|
13
|
+
* an attacker attempts, not seconds. A secret that can be guessed OFFLINE (vault's
|
|
14
|
+
* passphrase, which derives its encryption key) raises it at its own call site.
|
|
13
15
|
* Argon2id — what every caller hashes with — is what actually carries the cost
|
|
14
16
|
* of a guess here; these apps have exactly ONE principal and no enumerable user
|
|
15
17
|
* list, so the marginal value of characters 10–14 is small and the friction is
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* {@link resolveMinPasswordBytes} takes an optional override and **refuses to
|
|
26
28
|
* go below the default**. An app that genuinely runs at higher stakes says so
|
|
27
29
|
* at its own call site, in one visible argument, with a comment saying why.
|
|
28
|
-
*
|
|
30
|
+
* `vault` does (9, since 2026-09-24 — its passphrase is an encryption key). The retired orch companion app did (14, as the single
|
|
29
31
|
* internet-facing door into the host that runs `claude` in every repo) until
|
|
30
32
|
* the owner dropped it to the shared floor on 2026-07-26.
|
|
31
33
|
*
|
|
@@ -39,13 +41,13 @@
|
|
|
39
41
|
* they read, so a multi-byte character counts as more than one, which is the
|
|
40
42
|
* conservative direction).
|
|
41
43
|
*/
|
|
42
|
-
export const DEFAULT_MIN_PASSWORD_BYTES =
|
|
44
|
+
export const DEFAULT_MIN_PASSWORD_BYTES = 6;
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* The floor this app enforces: the default, or a higher one it asks for.
|
|
46
48
|
*
|
|
47
49
|
* Throws on an override below the default rather than silently clamping. A
|
|
48
|
-
* caller passing
|
|
50
|
+
* caller passing 4 believes it is enforcing 4; being handed 6 without a word is
|
|
49
51
|
* how a security floor becomes folklore.
|
|
50
52
|
*/
|
|
51
53
|
export function resolveMinPasswordBytes(override?: number): number {
|