laravel-workers 0.4.0 → 0.4.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.
@@ -87,4 +87,4 @@ test('encrypter timingSafeEqual zero-length handled', async () => {
87
87
  assert.equal(timingSafeEqual('', ''), true);
88
88
  assert.equal(timingSafeEqual(new Uint8Array(0), new Uint8Array(0)), true);
89
89
  assert.equal(timingSafeEqual('abc', 'abd'), false);
90
- });
90
+ });
@@ -1,149 +1,149 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- make,
5
- makeSync,
6
- check,
7
- needsRehash,
8
- isHashed,
9
- isHashedDigest,
10
- passwordInfo,
11
- verifyConfiguration,
12
- timingSafeEqual,
13
- } from '../hash.mjs';
14
-
15
- // PHP 8.3 password_get_info() oracle values (fresh-app vendor, 2026-10-19).
16
- const HASH10 = '$2y$10$cyT1llBUHIVeZFxw/hKj0eIeqIJNsMI0SUhpbh6K5FWh7NxcNqnf.';
17
- const HASH12 = '$2y$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
18
- const HASH14 = '$2y$14$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
19
- const HASH12_A = '$2a$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
20
- const HASH12_B = '$2b$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
21
- const SHORT_2Y = '$2y$10$abcdefghijklmnopqrstuv';
22
- const ARGON = '$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ$abc';
23
-
24
- test('hash round-trip (bcrypt)', async () => {
25
- const h = await make('hello', { rounds: 4 });
26
- // PHP 8.3 password_hash() emits `$2y$` - the kernel stores PHP's prefix
27
- // (bcryptjs emits `$2a$`), so the digest is what Laravel writes.
28
- assert.equal(h.slice(0, 7), '$2y$04$');
29
- assert.equal(await check('hello', h), true);
30
- assert.equal(await check('wrong', h), false);
31
- assert.equal(await check('hello', ''), false);
32
- });
33
-
34
- test('hash needsRehash', async () => {
35
- const h4 = await make('x', { rounds: 4 });
36
- assert.equal(needsRehash(h4, { rounds: 4 }), false);
37
- assert.equal(needsRehash(h4, { rounds: 10 }), true);
38
- assert.equal(needsRehash('not a hash', { rounds: 10 }), true);
39
- });
40
-
41
- test('isHashed detects bcrypt/argon2 digests', async () => {
42
- const h = await make('hello', { rounds: 4 });
43
- assert.equal(isHashed(h), true);
44
- // A TRUNCATED digest is not a hash (PHP's password_get_info() identifies
45
- // only a full `$2y$` bcrypt digest - but the argon2 prefix it does).
46
- assert.equal(isHashed('$2y$10$abcdefghijklmnopqrstuv'), false);
47
- assert.equal(isHashed('$argon2id$v=19$m=65536'), true);
48
- assert.equal(isHashed('plain'), false);
49
- assert.equal(isHashed(''), false);
50
- assert.equal(isHashed(null), false);
51
- assert.equal(isHashed(123), false);
52
- });
53
-
54
- test('hash empty/long unicode', async () => {
55
- for (const v of ['', 'a'.repeat(72), 'unicode: café — \u0007 emoji 👩‍💻']) {
56
- const h = await make(v, { rounds: 4 });
57
- assert.equal(await check(v, h), true);
58
- }
59
- });
60
-
61
- test('hash same password different salts both verify', async () => {
62
- const h1 = await make('same-password', { rounds: 4 });
63
- const h2 = await make('same-password', { rounds: 4 });
64
- assert.notEqual(h1, h2);
65
- assert.equal(await check('same-password', h1), true);
66
- assert.equal(await check('same-password', h2), true);
67
- });
68
-
69
- test('hash forged byte-flip fails', async () => {
70
- const h = await make('secret123', { rounds: 4 });
71
- const arr = h.split('');
72
- arr[20] = arr[20] === 'a' ? 'b' : 'a';
73
- const forged = arr.join('');
74
- assert.equal(await check('secret123', forged), false);
75
- });
76
-
77
- test('hash timingSafeEqual zero-length handled', async () => {
78
- assert.equal(timingSafeEqual('', ''), true);
79
- assert.equal(timingSafeEqual(new Uint8Array(0), new Uint8Array(0)), true);
80
- assert.equal(timingSafeEqual('', 'a'), false);
81
- assert.equal(timingSafeEqual('a', ''), false);
82
- assert.equal(timingSafeEqual('abc', 'abc'), true);
83
- assert.equal(timingSafeEqual('abc', 'abd'), false);
84
- assert.equal(timingSafeEqual(new Uint8Array([1,2,3]), new Uint8Array([1,2,3])), true);
85
- assert.equal(timingSafeEqual(new Uint8Array([1,2,3]), new Uint8Array([1,2,4])), false);
86
- });
87
-
88
- test('makeSync emits PHP 8.3 $2y$ digests and verifies', async () => {
89
- const h = makeSync('secret', { rounds: 4 });
90
- assert.equal(h.slice(0, 7), '$2y$04$');
91
- assert.equal(h.length, 60);
92
- assert.equal(await check('secret', h), true);
93
- assert.equal(await check('wrong', h), false);
94
- assert.equal(makeSync('secret', { rounds: 4 }).slice(0, 7), '$2y$04$');
95
- });
96
-
97
- test('passwordInfo mirrors PHP 8.3 password_get_info()', () => {
98
- assert.deepEqual(passwordInfo(HASH10), { algo: '2y', algoName: 'bcrypt', options: { cost: 10 } });
99
- assert.deepEqual(passwordInfo(HASH12), { algo: '2y', algoName: 'bcrypt', options: { cost: 12 } });
100
- assert.equal(passwordInfo(HASH12_A).algo, null);
101
- assert.equal(passwordInfo(HASH12_B).algo, null);
102
- assert.equal(passwordInfo(SHORT_2Y).algo, null);
103
- assert.equal(passwordInfo(ARGON).algo, 'argon2id');
104
- assert.equal(passwordInfo(ARGON).algoName, 'argon2id');
105
- assert.equal(passwordInfo('plain').algo, null);
106
- assert.equal(passwordInfo('').algo, null);
107
- assert.equal(passwordInfo(null).algo, null);
108
- assert.equal(passwordInfo(123).algo, null);
109
- });
110
-
111
- test('isHashedDigest uses exact HashManager::isHashed semantics', () => {
112
- assert.equal(isHashedDigest(HASH10), true);
113
- assert.equal(isHashedDigest(HASH12), true);
114
- assert.equal(isHashedDigest(HASH12_A), false);
115
- assert.equal(isHashedDigest(HASH12_B), false);
116
- assert.equal(isHashedDigest(SHORT_2Y), false);
117
- assert.equal(isHashedDigest(ARGON), true);
118
- assert.equal(isHashedDigest('plain'), false);
119
- assert.equal(isHashedDigest(null), false);
120
- // isHashed() is the same rule as isHashedDigest() (Laravel's
121
- // Hash::isHashed() is `$this->driver()->info($value)['algo'] !== null`).
122
- assert.equal(isHashed(HASH12_A), false);
123
- assert.equal(isHashed(SHORT_2Y), false);
124
- });
125
-
126
- test('verifyConfiguration accepts cost <= rounds and rejects others', () => {
127
- assert.equal(verifyConfiguration(HASH10, { rounds: 12 }), true);
128
- assert.equal(verifyConfiguration(HASH12, { rounds: 12 }), true);
129
- assert.equal(verifyConfiguration(HASH14, { rounds: 12 }), false);
130
- assert.equal(verifyConfiguration(HASH12, { rounds: 10 }), false);
131
- assert.equal(verifyConfiguration(HASH12_A, { rounds: 12 }), false);
132
- assert.equal(verifyConfiguration(SHORT_2Y, { rounds: 12 }), false);
133
- assert.equal(verifyConfiguration(ARGON, { rounds: 12 }), false);
134
- assert.equal(verifyConfiguration('plain', { rounds: 12 }), false);
135
- assert.equal(verifyConfiguration(null, { rounds: 12 }), false);
136
- });
137
-
138
- test('isHashed() follows password_get_info (Laravel: Hash::isHashed)', () => {
139
- // Laravel's Hash::isHashed() is `$this->driver()->info($value)['algo'] !==
140
- // null` - the same rule as PHP's password_get_info(), which identifies only a
141
- // FULL `$2y$` bcrypt digest (or an argon2 one). A `$2a$`/`$2b$` prefix or a
142
- // truncated digest is NOT a hash.
143
- const full = '$2y$10$' + 'a'.repeat(53);
144
- assert.equal(isHashed(full), true);
145
- assert.equal(isHashed('$2a$10$' + 'a'.repeat(53)), false);
146
- assert.equal(isHashed('$2b$10$' + 'a'.repeat(53)), false);
147
- assert.equal(isHashed('$2y$10$short'), false);
148
- assert.equal(isHashed('plain'), false);
149
- });
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ make,
5
+ makeSync,
6
+ check,
7
+ needsRehash,
8
+ isHashed,
9
+ isHashedDigest,
10
+ passwordInfo,
11
+ verifyConfiguration,
12
+ timingSafeEqual,
13
+ } from '../hash.mjs';
14
+
15
+ // PHP 8.3 password_get_info() oracle values (fresh-app vendor, 2026-10-19).
16
+ const HASH10 = '$2y$10$cyT1llBUHIVeZFxw/hKj0eIeqIJNsMI0SUhpbh6K5FWh7NxcNqnf.';
17
+ const HASH12 = '$2y$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
18
+ const HASH14 = '$2y$14$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
19
+ const HASH12_A = '$2a$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
20
+ const HASH12_B = '$2b$12$d0pIpZcCKtpClnDI9BCFgeYNp2hPFUuoGalG4focVyYVTL01s2ZmW';
21
+ const SHORT_2Y = '$2y$10$abcdefghijklmnopqrstuv';
22
+ const ARGON = '$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ$abc';
23
+
24
+ test('hash round-trip (bcrypt)', async () => {
25
+ const h = await make('hello', { rounds: 4 });
26
+ // PHP 8.3 password_hash() emits `$2y$` - the kernel stores PHP's prefix
27
+ // (bcryptjs emits `$2a$`), so the digest is what Laravel writes.
28
+ assert.equal(h.slice(0, 7), '$2y$04$');
29
+ assert.equal(await check('hello', h), true);
30
+ assert.equal(await check('wrong', h), false);
31
+ assert.equal(await check('hello', ''), false);
32
+ });
33
+
34
+ test('hash needsRehash', async () => {
35
+ const h4 = await make('x', { rounds: 4 });
36
+ assert.equal(needsRehash(h4, { rounds: 4 }), false);
37
+ assert.equal(needsRehash(h4, { rounds: 10 }), true);
38
+ assert.equal(needsRehash('not a hash', { rounds: 10 }), true);
39
+ });
40
+
41
+ test('isHashed detects bcrypt/argon2 digests', async () => {
42
+ const h = await make('hello', { rounds: 4 });
43
+ assert.equal(isHashed(h), true);
44
+ // A TRUNCATED digest is not a hash (PHP's password_get_info() identifies
45
+ // only a full `$2y$` bcrypt digest - but the argon2 prefix it does).
46
+ assert.equal(isHashed('$2y$10$abcdefghijklmnopqrstuv'), false);
47
+ assert.equal(isHashed('$argon2id$v=19$m=65536'), true);
48
+ assert.equal(isHashed('plain'), false);
49
+ assert.equal(isHashed(''), false);
50
+ assert.equal(isHashed(null), false);
51
+ assert.equal(isHashed(123), false);
52
+ });
53
+
54
+ test('hash empty/long unicode', async () => {
55
+ for (const v of ['', 'a'.repeat(72), 'unicode: café — \u0007 emoji 👩‍💻']) {
56
+ const h = await make(v, { rounds: 4 });
57
+ assert.equal(await check(v, h), true);
58
+ }
59
+ });
60
+
61
+ test('hash same password different salts both verify', async () => {
62
+ const h1 = await make('same-password', { rounds: 4 });
63
+ const h2 = await make('same-password', { rounds: 4 });
64
+ assert.notEqual(h1, h2);
65
+ assert.equal(await check('same-password', h1), true);
66
+ assert.equal(await check('same-password', h2), true);
67
+ });
68
+
69
+ test('hash forged byte-flip fails', async () => {
70
+ const h = await make('secret123', { rounds: 4 });
71
+ const arr = h.split('');
72
+ arr[20] = arr[20] === 'a' ? 'b' : 'a';
73
+ const forged = arr.join('');
74
+ assert.equal(await check('secret123', forged), false);
75
+ });
76
+
77
+ test('hash timingSafeEqual zero-length handled', async () => {
78
+ assert.equal(timingSafeEqual('', ''), true);
79
+ assert.equal(timingSafeEqual(new Uint8Array(0), new Uint8Array(0)), true);
80
+ assert.equal(timingSafeEqual('', 'a'), false);
81
+ assert.equal(timingSafeEqual('a', ''), false);
82
+ assert.equal(timingSafeEqual('abc', 'abc'), true);
83
+ assert.equal(timingSafeEqual('abc', 'abd'), false);
84
+ assert.equal(timingSafeEqual(new Uint8Array([1,2,3]), new Uint8Array([1,2,3])), true);
85
+ assert.equal(timingSafeEqual(new Uint8Array([1,2,3]), new Uint8Array([1,2,4])), false);
86
+ });
87
+
88
+ test('makeSync emits PHP 8.3 $2y$ digests and verifies', async () => {
89
+ const h = makeSync('secret', { rounds: 4 });
90
+ assert.equal(h.slice(0, 7), '$2y$04$');
91
+ assert.equal(h.length, 60);
92
+ assert.equal(await check('secret', h), true);
93
+ assert.equal(await check('wrong', h), false);
94
+ assert.equal(makeSync('secret', { rounds: 4 }).slice(0, 7), '$2y$04$');
95
+ });
96
+
97
+ test('passwordInfo mirrors PHP 8.3 password_get_info()', () => {
98
+ assert.deepEqual(passwordInfo(HASH10), { algo: '2y', algoName: 'bcrypt', options: { cost: 10 } });
99
+ assert.deepEqual(passwordInfo(HASH12), { algo: '2y', algoName: 'bcrypt', options: { cost: 12 } });
100
+ assert.equal(passwordInfo(HASH12_A).algo, null);
101
+ assert.equal(passwordInfo(HASH12_B).algo, null);
102
+ assert.equal(passwordInfo(SHORT_2Y).algo, null);
103
+ assert.equal(passwordInfo(ARGON).algo, 'argon2id');
104
+ assert.equal(passwordInfo(ARGON).algoName, 'argon2id');
105
+ assert.equal(passwordInfo('plain').algo, null);
106
+ assert.equal(passwordInfo('').algo, null);
107
+ assert.equal(passwordInfo(null).algo, null);
108
+ assert.equal(passwordInfo(123).algo, null);
109
+ });
110
+
111
+ test('isHashedDigest uses exact HashManager::isHashed semantics', () => {
112
+ assert.equal(isHashedDigest(HASH10), true);
113
+ assert.equal(isHashedDigest(HASH12), true);
114
+ assert.equal(isHashedDigest(HASH12_A), false);
115
+ assert.equal(isHashedDigest(HASH12_B), false);
116
+ assert.equal(isHashedDigest(SHORT_2Y), false);
117
+ assert.equal(isHashedDigest(ARGON), true);
118
+ assert.equal(isHashedDigest('plain'), false);
119
+ assert.equal(isHashedDigest(null), false);
120
+ // isHashed() is the same rule as isHashedDigest() (Laravel's
121
+ // Hash::isHashed() is `$this->driver()->info($value)['algo'] !== null`).
122
+ assert.equal(isHashed(HASH12_A), false);
123
+ assert.equal(isHashed(SHORT_2Y), false);
124
+ });
125
+
126
+ test('verifyConfiguration accepts cost <= rounds and rejects others', () => {
127
+ assert.equal(verifyConfiguration(HASH10, { rounds: 12 }), true);
128
+ assert.equal(verifyConfiguration(HASH12, { rounds: 12 }), true);
129
+ assert.equal(verifyConfiguration(HASH14, { rounds: 12 }), false);
130
+ assert.equal(verifyConfiguration(HASH12, { rounds: 10 }), false);
131
+ assert.equal(verifyConfiguration(HASH12_A, { rounds: 12 }), false);
132
+ assert.equal(verifyConfiguration(SHORT_2Y, { rounds: 12 }), false);
133
+ assert.equal(verifyConfiguration(ARGON, { rounds: 12 }), false);
134
+ assert.equal(verifyConfiguration('plain', { rounds: 12 }), false);
135
+ assert.equal(verifyConfiguration(null, { rounds: 12 }), false);
136
+ });
137
+
138
+ test('isHashed() follows password_get_info (Laravel: Hash::isHashed)', () => {
139
+ // Laravel's Hash::isHashed() is `$this->driver()->info($value)['algo'] !==
140
+ // null` - the same rule as PHP's password_get_info(), which identifies only a
141
+ // FULL `$2y$` bcrypt digest (or an argon2 one). A `$2a$`/`$2b$` prefix or a
142
+ // truncated digest is NOT a hash.
143
+ const full = '$2y$10$' + 'a'.repeat(53);
144
+ assert.equal(isHashed(full), true);
145
+ assert.equal(isHashed('$2a$10$' + 'a'.repeat(53)), false);
146
+ assert.equal(isHashed('$2b$10$' + 'a'.repeat(53)), false);
147
+ assert.equal(isHashed('$2y$10$short'), false);
148
+ assert.equal(isHashed('plain'), false);
149
+ });