Skip to content

Commit

Permalink
Add isValidId(): check if a given string is a valid Dato URL-safe B…
Browse files Browse the repository at this point in the history
…ase64-encoded ID
  • Loading branch information
arcataroger authored and stefanoverna committed Mar 26, 2024
1 parent f2ef81a commit 51d39b1
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 34 deletions.
15 changes: 15 additions & 0 deletions packages/cma-client/__tests__/idUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { generateId, isValidId } from '../src/idUtils';

describe('generateId', () => {
it('expects an ID from generateId() to validate as a DatoCMS ID', () => {
expect(isValidId(generateId())).toBe(true);
});
});

describe('isValidId', () => {
it('checks if passed string is a valid DatoCMS ID', () => {
expect(isValidId('')).toBe(false);
expect(isValidId('foobar')).toBe(false);
expect(isValidId('WTyssHtyTzu9_EbszSVhPw')).toBe(true);
});
});
29 changes: 0 additions & 29 deletions packages/cma-client/src/generateId.ts

This file was deleted.

83 changes: 83 additions & 0 deletions packages/cma-client/src/idUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { v4 } from "uuid";

const decoder = new TextDecoder("utf8");

function fromUint8ArrayToUrlSafeBase64(bytes: Uint8Array) {
const base64 =
typeof Buffer === "undefined"
? btoa(decoder.decode(bytes))
: Buffer.from(bytes).toString("base64");

// Convert to URL-safe format (see RFC 4648, sec. 5)
return (
base64
// Replace + with -
.replace(/\+/g, "-")
// Replace / with _
.replace(/\//g, "_")
// Drop '==' padding
.substring(0, 22)
);
}

function fromUrlSafeBase64toUint8Array(urlSafeBase64: string): Uint8Array {
// Convert from URL-safe format (see RFC 4648, sec. 5)
const base64 = urlSafeBase64
// Replace - with +
.replace(/-/g, "+")
// Replace _ with /
.replace(/_/g, "/");

return typeof Buffer === "undefined"
? Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
: new Uint8Array(Buffer.from(base64, "base64"));
}

export function isValidId(id: string) {
const bytes = fromUrlSafeBase64toUint8Array(id);

// UUIDs are 16 bytes
if (bytes.length !== 16) {
return false;
}

// The variant field determines the layout of the UUID
// (see RFC 4122, sec. 4.1.1)

const variant = bytes.at(8)!;

// Variant must be the one described in RFC 4122
if ((variant & 0b11000000) !== 0b10000000) {
return false;
}

// The version number is in the most significant 4 bits
// of the time stamp (see RFC 4122, sec. 4.1.3)

const version = bytes.at(6)! >> 4;

// Version number must be 4 (randomly generated)
if (version !== 0x4) {
return false;
}

return true;
}

export function generateId() {
const bytes = v4(null, new Uint8Array(16));

// Here we unset the first bit to ensure [A-Za-f] as the first char.
//
// If we didn't do this, we would generate IDs that, once encoded
// in base64, could start with a '+' or a '/'. This makes them less
// easy to copy/paste, with bad DX.

// This choice is purely aesthetic: definitely non-mandatory!

bytes[0] = bytes[0]! & 0x7f;

const base64 = fromUint8ArrayToUrlSafeBase64(bytes);

return base64;
}
10 changes: 5 additions & 5 deletions packages/cma-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export { ApiError, TimeoutError, LogLevel } from '@datocms/rest-client-utils';
export { ApiError, LogLevel, TimeoutError } from '@datocms/rest-client-utils';
export * from './buildBlockRecord';
export * from './buildClient';
export { Client } from './generated/Client';
export * as Resources from './generated/resources';
export type { ClientConfigOptions } from './generated/Client';
export * from './buildClient';
export * from './buildBlockRecord';
export * from './generateId';
export * as Resources from './generated/resources';
export * as SchemaTypes from './generated/SchemaTypes';
export * as SimpleSchemaTypes from './generated/SimpleSchemaTypes';
export * from './idUtils';

0 comments on commit 51d39b1

Please sign in to comment.