[feat](client): generate a cryptographic per-install device id
The device id is the subscriber-only pseudonymous handle, used as the join idempotency key and to address this device in the delivery ledger. Use SecureRandom through the native bridge, validate it against the service limits, and keep hardware identifiers out of it entirely. Device labels describe the device role, never a slice of the id, and the subscriber list renders the wire id/created/lastSeenAt fields.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
export {};
|
||||
|
||||
/**
|
||||
* Subscriber rows must never depend on a stable device identifier.
|
||||
*
|
||||
* The service stopped serialising `deviceId` (privacy ruling); the admin still
|
||||
* has to tell two phones apart well enough to revoke the right one, without any
|
||||
* token fragment or identifier. This is a source contract because the row is
|
||||
* JSX with no headless renderer in this project.
|
||||
*/
|
||||
declare const __dirname: string;
|
||||
const fs: { readFileSync(p: string, enc: string): string } = jest.requireActual('fs');
|
||||
const path: { join(...parts: string[]): string } = jest.requireActual('path');
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'src', 'screens', 'AlertDetailScreen.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('subscriber row discriminator', () => {
|
||||
it('does not read deviceId or the removed joined field', () => {
|
||||
expect(source).not.toMatch(/sub\.deviceId/);
|
||||
expect(source).not.toMatch(/sub\.joined\b/);
|
||||
});
|
||||
|
||||
it('uses the subscription id as the revocation handle', () => {
|
||||
expect(source).toMatch(/key=\{sub\.id\}/);
|
||||
expect(source).toMatch(/subscriptionId: sub\.id/);
|
||||
});
|
||||
|
||||
it('only reloads after a successful revoke and surfaces failures', () => {
|
||||
const pressAt = source.indexOf('onPress={async () => {');
|
||||
expect(pressAt).toBeGreaterThan(-1);
|
||||
const handler = source.slice(pressAt, pressAt + 700);
|
||||
// reloadSubs() lives in the try, after the awaited removal, so the row
|
||||
// cannot vanish optimistically when the service rejects the request.
|
||||
expect(handler.indexOf('await api.removeSubscription')).toBeLessThan(
|
||||
handler.indexOf('reloadSubs()'),
|
||||
);
|
||||
expect(handler).toContain('setError(');
|
||||
});
|
||||
|
||||
it('names a device by role label + server order, never a token fragment', () => {
|
||||
const row = source.slice(source.indexOf('const deviceName'), source.indexOf('const deviceName') + 900);
|
||||
expect(row).toMatch(/index \+ 1/);
|
||||
expect(row).toMatch(/joined \{joinedAgo\}/);
|
||||
expect(row).not.toMatch(/token/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user