@oxy.so/protocol 1.0.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/chain/continuity.js +54 -0
- package/dist/cjs/chain/engine.js +34 -0
- package/dist/cjs/chain/recordStore.js +25 -0
- package/dist/cjs/chain/types.js +22 -0
- package/dist/cjs/chain/verify.js +82 -0
- package/dist/cjs/envelope/canonicalJson.js +107 -0
- package/dist/cjs/envelope/recordId.js +60 -0
- package/dist/cjs/envelope/sign.js +75 -0
- package/dist/cjs/envelope/signingInput.js +32 -0
- package/dist/cjs/identity/resolver.js +50 -0
- package/dist/cjs/index.js +71 -0
- package/dist/cjs/node/constants.js +85 -0
- package/dist/cjs/node/didWebResolver.js +126 -0
- package/dist/cjs/node/httpFetch.js +61 -0
- package/dist/cjs/node/index.js +71 -0
- package/dist/cjs/node/nodeApp.js +344 -0
- package/dist/cjs/node/nodeClient.js +204 -0
- package/dist/cjs/node/rateLimit.js +187 -0
- package/dist/cjs/node/verifyRecord.js +51 -0
- package/dist/cjs/platform/crypto.js +186 -0
- package/dist/cjs/platform/crypto.native.js +204 -0
- package/dist/cjs/platform/expoTypes.js +24 -0
- package/dist/cjs/platform/platform.js +33 -0
- package/dist/cjs/secp256k1.js +148 -0
- package/dist/cjs/transparency/checkpoint.js +79 -0
- package/dist/cjs/transparency/tree.js +197 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/chain/continuity.js +51 -0
- package/dist/esm/chain/engine.js +31 -0
- package/dist/esm/chain/recordStore.js +24 -0
- package/dist/esm/chain/types.js +19 -0
- package/dist/esm/chain/verify.js +78 -0
- package/dist/esm/envelope/canonicalJson.js +104 -0
- package/dist/esm/envelope/recordId.js +56 -0
- package/dist/esm/envelope/sign.js +69 -0
- package/dist/esm/envelope/signingInput.js +29 -0
- package/dist/esm/identity/resolver.js +47 -0
- package/dist/esm/index.js +36 -0
- package/dist/esm/node/constants.js +82 -0
- package/dist/esm/node/didWebResolver.js +122 -0
- package/dist/esm/node/httpFetch.js +55 -0
- package/dist/esm/node/index.js +28 -0
- package/dist/esm/node/nodeApp.js +336 -0
- package/dist/esm/node/nodeClient.js +198 -0
- package/dist/esm/node/rateLimit.js +182 -0
- package/dist/esm/node/verifyRecord.js +48 -0
- package/dist/esm/platform/crypto.js +145 -0
- package/dist/esm/platform/crypto.native.js +196 -0
- package/dist/esm/platform/expoTypes.js +23 -0
- package/dist/esm/platform/platform.js +29 -0
- package/dist/esm/secp256k1.js +137 -0
- package/dist/esm/transparency/checkpoint.js +73 -0
- package/dist/esm/transparency/tree.js +189 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/chain/continuity.d.ts +28 -0
- package/dist/types/chain/engine.d.ts +27 -0
- package/dist/types/chain/recordStore.d.ts +85 -0
- package/dist/types/chain/types.d.ts +79 -0
- package/dist/types/chain/verify.d.ts +45 -0
- package/dist/types/envelope/canonicalJson.d.ts +44 -0
- package/dist/types/envelope/recordId.d.ts +30 -0
- package/dist/types/envelope/sign.d.ts +47 -0
- package/dist/types/envelope/signingInput.d.ts +33 -0
- package/dist/types/identity/resolver.d.ts +67 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/node/constants.d.ts +80 -0
- package/dist/types/node/didWebResolver.d.ts +47 -0
- package/dist/types/node/httpFetch.d.ts +60 -0
- package/dist/types/node/index.d.ts +28 -0
- package/dist/types/node/nodeApp.d.ts +120 -0
- package/dist/types/node/nodeClient.d.ts +135 -0
- package/dist/types/node/rateLimit.d.ts +95 -0
- package/dist/types/node/verifyRecord.d.ts +41 -0
- package/dist/types/platform/crypto.d.ts +93 -0
- package/dist/types/platform/crypto.native.d.ts +77 -0
- package/dist/types/platform/expoTypes.d.ts +99 -0
- package/dist/types/platform/platform.d.ts +25 -0
- package/dist/types/secp256k1.d.ts +45 -0
- package/dist/types/transparency/checkpoint.d.ts +71 -0
- package/dist/types/transparency/tree.d.ts +135 -0
- package/package.json +157 -0
- package/src/__tests__/canonicalJson.test.ts +116 -0
- package/src/__tests__/chain.test.ts +279 -0
- package/src/__tests__/didWebResolver.test.ts +132 -0
- package/src/__tests__/envelope.test.ts +267 -0
- package/src/__tests__/nodeApp.test.ts +410 -0
- package/src/__tests__/nodeClient.test.ts +177 -0
- package/src/__tests__/nodeHarness.ts +151 -0
- package/src/__tests__/optionalNativePeers.test.ts +233 -0
- package/src/__tests__/rateLimit.test.ts +268 -0
- package/src/__tests__/runnerGuard.test.ts +85 -0
- package/src/__tests__/secp256k1.test.ts +118 -0
- package/src/__tests__/transparency.test.ts +353 -0
- package/src/chain/continuity.ts +59 -0
- package/src/chain/engine.ts +43 -0
- package/src/chain/recordStore.ts +98 -0
- package/src/chain/types.ts +85 -0
- package/src/chain/verify.ts +102 -0
- package/src/envelope/canonicalJson.ts +120 -0
- package/src/envelope/recordId.ts +63 -0
- package/src/envelope/sign.ts +86 -0
- package/src/envelope/signingInput.ts +48 -0
- package/src/identity/resolver.ts +90 -0
- package/src/index.ts +101 -0
- package/src/node/constants.ts +105 -0
- package/src/node/didWebResolver.ts +162 -0
- package/src/node/httpFetch.ts +88 -0
- package/src/node/index.ts +87 -0
- package/src/node/nodeApp.ts +471 -0
- package/src/node/nodeClient.ts +322 -0
- package/src/node/rateLimit.ts +233 -0
- package/src/node/verifyRecord.ts +60 -0
- package/src/platform/crypto.native.ts +251 -0
- package/src/platform/crypto.ts +172 -0
- package/src/platform/expoTypes.ts +99 -0
- package/src/platform/platform.ts +31 -0
- package/src/secp256k1.ts +207 -0
- package/src/transparency/checkpoint.ts +109 -0
- package/src/transparency/tree.ts +258 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Crypto / Storage — React Native Variant
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./crypto.ts`. See the doc-comment at the top of that file for
|
|
5
|
+
* the full design.
|
|
6
|
+
*
|
|
7
|
+
* Metro auto-selects this file in any non-web build (`preferNativePlatform`
|
|
8
|
+
* is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
|
|
9
|
+
* source-extension resolution inside `node_modules/@oxy.so/protocol/dist/`). On
|
|
10
|
+
* iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
|
|
11
|
+
* if they existed, but they don't — `.native.js` is the shared RN variant.
|
|
12
|
+
*
|
|
13
|
+
* - The default variant references Node's `'crypto'` and would crash Metro
|
|
14
|
+
* if bundled into an RN app.
|
|
15
|
+
* - This variant references the RN-only modules (`expo-crypto`,
|
|
16
|
+
* `expo-secure-store`, `@react-native-async-storage/async-storage`),
|
|
17
|
+
* each behind Metro's optional-dependency mechanism (see below).
|
|
18
|
+
*
|
|
19
|
+
* Both variants expose the same surface; importers don't care which one
|
|
20
|
+
* they got.
|
|
21
|
+
*
|
|
22
|
+
* # Why `try { require('literal') } catch` and not a static import?
|
|
23
|
+
*
|
|
24
|
+
* Those three RN modules are declared OPTIONAL peer dependencies in
|
|
25
|
+
* `package.json`. A static `import` contradicts that: an optional peer that is
|
|
26
|
+
* omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
|
|
27
|
+
* bundle. Because `@oxy.so/core`'s `crypto/polyfill` imports `@oxy.so/protocol`
|
|
28
|
+
* from its root entry, this file is in the eager graph of EVERY React Native
|
|
29
|
+
* app on `@oxy.so/core` — so a single undeclared optional peer broke the native
|
|
30
|
+
* bundle of every app that did not happen to install it, with a resolution
|
|
31
|
+
* error pointing at a dependency the app never mentions.
|
|
32
|
+
*
|
|
33
|
+
* Metro treats a `require()` of a STRING LITERAL that sits inside a `try`
|
|
34
|
+
* block as an optional dependency: it resolves it when present, and when
|
|
35
|
+
* absent emits a stub that throws on evaluation instead of failing the build.
|
|
36
|
+
* The `catch` turns that into a `null` module handle, and the loader below
|
|
37
|
+
* throws an actionable error naming the missing package the first time the
|
|
38
|
+
* capability is actually used. Bundle-time hard failure becomes a
|
|
39
|
+
* capability-scoped runtime failure — which is exactly what "optional peer"
|
|
40
|
+
* is supposed to mean.
|
|
41
|
+
*
|
|
42
|
+
* Two constraints this shape has to respect, both learned the hard way:
|
|
43
|
+
*
|
|
44
|
+
* - The specifier MUST be a literal. A runtime-computed `require(variable)`
|
|
45
|
+
* is unresolvable for Metro (that is the bug the shared-identity bridge
|
|
46
|
+
* below documents) and silently yields nothing in a consuming repo.
|
|
47
|
+
* - The load MUST stay synchronous. `getRandomBytesRN` backs
|
|
48
|
+
* `globalThis.crypto.getRandomValues` in `@oxy.so/core`'s polyfill, which
|
|
49
|
+
* cannot await anything.
|
|
50
|
+
*
|
|
51
|
+
* `expo-modules-core` is a NON-optional peer (every RN app has it via `expo`),
|
|
52
|
+
* so it stays a plain static import.
|
|
53
|
+
*/
|
|
54
|
+
import type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge } from './expoTypes';
|
|
55
|
+
export type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge };
|
|
56
|
+
/** Persistent KV storage surface used from `@react-native-async-storage/async-storage`. */
|
|
57
|
+
type AsyncStorageLike = {
|
|
58
|
+
getItem: (key: string) => Promise<string | null>;
|
|
59
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
60
|
+
removeItem: (key: string) => Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
export declare function loadNodeCrypto(): Promise<typeof import('crypto')>;
|
|
63
|
+
export declare function loadExpoCrypto(): Promise<ExpoCryptoLike>;
|
|
64
|
+
export declare function loadSecureStore(): Promise<ExpoSecureStoreLike>;
|
|
65
|
+
export declare function loadAsyncStorage(): Promise<{
|
|
66
|
+
default: AsyncStorageLike;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Synchronous random-bytes via `expo-crypto.getRandomBytes`.
|
|
70
|
+
*
|
|
71
|
+
* Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
|
|
72
|
+
* `globalThis.crypto.getRandomValues`, which cannot await. That is why
|
|
73
|
+
* `expo-crypto` is resolved with a synchronous `require` at module scope rather
|
|
74
|
+
* than a dynamic `import()`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getRandomBytesRN(byteCount: number): Uint8Array;
|
|
77
|
+
export declare function loadSharedIdentityBridge(): Promise<SharedIdentityBridge | null>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural interfaces for Expo platform modules.
|
|
3
|
+
*
|
|
4
|
+
* These replace `typeof import('expo-crypto')` and
|
|
5
|
+
* `typeof import('expo-secure-store')` in the built declaration files of
|
|
6
|
+
* `@oxy.so/protocol` and `@oxy.so/core`.
|
|
7
|
+
*
|
|
8
|
+
* ## Why structural interfaces instead of `typeof import('expo-*')`?
|
|
9
|
+
*
|
|
10
|
+
* Under NodeNext module resolution (used by `@oxy.so/api` and `@oxy.so/node`),
|
|
11
|
+
* `expo-crypto` ships with `"exports": {}` (empty exports map). TypeScript
|
|
12
|
+
* traverses into the package anyway via the `types` field, which transitively
|
|
13
|
+
* loads `expo-modules-core`. That pollution makes `setInterval`/`setTimeout`
|
|
14
|
+
* resolve to DOM's `number` return type rather than Node's `NodeJS.Timeout`,
|
|
15
|
+
* producing ~10 spurious `TS2322` / `TS2339` errors in every consumer that
|
|
16
|
+
* uses Node timer APIs — none of which reference protocol types at all.
|
|
17
|
+
*
|
|
18
|
+
* Structural interfaces break the transitive expo-modules-core dependency
|
|
19
|
+
* entirely: consumers that don't have Expo installed see clean types, and
|
|
20
|
+
* the actual RN runtime (which DOES have Expo installed) still works because
|
|
21
|
+
* the real modules satisfy these interfaces structurally.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Minimal structural interface for the subset of `expo-crypto` used by
|
|
25
|
+
* `@oxy.so/protocol` (SHA-256 hashing in RN) and `@oxy.so/core` (key-manager
|
|
26
|
+
* random-byte generation).
|
|
27
|
+
*
|
|
28
|
+
* The real `expo-crypto` namespace satisfies this interface structurally.
|
|
29
|
+
*/
|
|
30
|
+
export interface ExpoCryptoLike {
|
|
31
|
+
/** Generate `byteCount` cryptographically-random bytes synchronously. */
|
|
32
|
+
getRandomBytes(byteCount: number): Uint8Array;
|
|
33
|
+
/** Generate `byteCount` cryptographically-random bytes asynchronously. */
|
|
34
|
+
getRandomBytesAsync(byteCount: number): Promise<Uint8Array>;
|
|
35
|
+
/**
|
|
36
|
+
* Compute a digest of `data` using the given `algorithm` string
|
|
37
|
+
* (e.g. `CryptoDigestAlgorithm.SHA256`). Used by `recordId.ts` in the
|
|
38
|
+
* React Native runtime path for content-address hashing.
|
|
39
|
+
*/
|
|
40
|
+
digestStringAsync(algorithm: string, data: string, options?: unknown): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Algorithm constants (e.g. `CryptoDigestAlgorithm.SHA256 === 'SHA-256'`).
|
|
43
|
+
* Represented as a plain string-keyed record so the interface does not
|
|
44
|
+
* depend on the enum definition inside expo-crypto.
|
|
45
|
+
*/
|
|
46
|
+
readonly CryptoDigestAlgorithm: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimal structural interface for the subset of `expo-secure-store` used by
|
|
50
|
+
* `@oxy.so/core` `KeyManager` for on-device identity storage.
|
|
51
|
+
*
|
|
52
|
+
* The real `expo-secure-store` namespace satisfies this interface structurally.
|
|
53
|
+
*
|
|
54
|
+
* `options` are typed as `object` (rather than the concrete `SecureStoreOptions`
|
|
55
|
+
* from expo-secure-store) so callers can pass any plain options bag without
|
|
56
|
+
* importing expo-secure-store's type declarations. TypeScript method bivariance
|
|
57
|
+
* makes the real `setItemAsync(opts?: SecureStoreOptions)` compatible with this
|
|
58
|
+
* `setItemAsync(opts?: object)` signature.
|
|
59
|
+
*/
|
|
60
|
+
export interface ExpoSecureStoreLike {
|
|
61
|
+
setItemAsync(key: string, value: string, options?: object): Promise<void>;
|
|
62
|
+
getItemAsync(key: string, options?: object): Promise<string | null>;
|
|
63
|
+
deleteItemAsync(key: string, options?: object): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Keychain / Keystore accessibility constant: item accessible only when the
|
|
66
|
+
* device is unlocked, and only on this device (no iCloud backup).
|
|
67
|
+
* Value: `KeychainAccessibilityConstant` (a number alias in expo-secure-store).
|
|
68
|
+
*/
|
|
69
|
+
readonly WHEN_UNLOCKED_THIS_DEVICE_ONLY: number;
|
|
70
|
+
/**
|
|
71
|
+
* Keychain / Keystore accessibility constant: item accessible whenever the
|
|
72
|
+
* device is unlocked (may be restored to a different device via backup).
|
|
73
|
+
*/
|
|
74
|
+
readonly WHEN_UNLOCKED: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Structural interface for the `@oxy.so/expo-oxy-identity` native module — the
|
|
78
|
+
* cross-app shared Oxy identity bridge.
|
|
79
|
+
*
|
|
80
|
+
* On Android the keypair crosses the process boundary through a
|
|
81
|
+
* signature-protected `ContentProvider` hosted by Commons; on iOS every method
|
|
82
|
+
* is a no-op (the Keychain Access Group path in `@oxy.so/core`'s `KeyManager`
|
|
83
|
+
* owns iOS sharing). Typed structurally here so `@oxy.so/protocol` and
|
|
84
|
+
* `@oxy.so/core` can reference the bridge without a hard dependency on the
|
|
85
|
+
* optional native module.
|
|
86
|
+
*/
|
|
87
|
+
export interface SharedIdentityBridge {
|
|
88
|
+
/** Read the shared keypair, or null when none is available on this device. */
|
|
89
|
+
getShared(): Promise<{
|
|
90
|
+
privateKey: string;
|
|
91
|
+
publicKey: string;
|
|
92
|
+
} | null>;
|
|
93
|
+
/** Persist the shared keypair into this app's hardware-backed store (Commons only). */
|
|
94
|
+
putShared(privateKey: string, publicKey: string): Promise<void>;
|
|
95
|
+
/** Whether a shared identity is readable on this device. */
|
|
96
|
+
hasShared(): Promise<boolean>;
|
|
97
|
+
/** Remove the shared identity from this app's local store (best-effort). */
|
|
98
|
+
clearShared(): Promise<void>;
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Detection — runtime predicates.
|
|
3
|
+
*
|
|
4
|
+
* Detects the host runtime WITHOUT importing from 'react-native', so the
|
|
5
|
+
* protocol's crypto modules can be used in web / Node.js / React Native
|
|
6
|
+
* environments without bundlers failing on react-native imports.
|
|
7
|
+
*
|
|
8
|
+
* Only the two predicates the protocol's platform-crypto loaders need live
|
|
9
|
+
* here. Richer platform detection (`getPlatformOS`, `isWeb`, `isNative`, …)
|
|
10
|
+
* is an SDK concern and stays in `@oxy.so/core`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Check if running in React Native.
|
|
14
|
+
*
|
|
15
|
+
* Selects the React Native crypto variant (`expo-crypto` /
|
|
16
|
+
* `expo-secure-store` / async-storage) over the Node/web variant.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isReactNative(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Check if running in Node.js.
|
|
21
|
+
*
|
|
22
|
+
* Gates use of Node's built-in `crypto` (the synchronous SHA-256 path and
|
|
23
|
+
* `randomBytes`) and the `await import('node:crypto')` loader.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isNodeJS(): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secp256k1 primitives shared by every Oxy runtime.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the curve binding and the wire formats used across Oxy:
|
|
5
|
+
* 32-byte private keys, compressed or uncompressed SEC1 public keys,
|
|
6
|
+
* RFC 6979 deterministic ECDSA signatures encoded as DER, and the 32-byte
|
|
7
|
+
* ECDH x-coordinate. Callers never receive a library-specific key object.
|
|
8
|
+
*/
|
|
9
|
+
export interface Secp256k1KeyPair {
|
|
10
|
+
/** Canonical lowercase 32-byte scalar. */
|
|
11
|
+
privateKey: string;
|
|
12
|
+
/** Lowercase uncompressed SEC1 public key (65 bytes). */
|
|
13
|
+
publicKey: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Secp256k1SignatureOptions {
|
|
16
|
+
/** Normalize S into the lower half of the curve order. Defaults to false for wire compatibility. */
|
|
17
|
+
lowS?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a legacy short/cased scalar to canonical 32-byte lowercase hex.
|
|
21
|
+
* The scalar must be within the secp256k1 order; zero and overflow are rejected.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeSecp256k1PrivateKey(privateKeyHex: string): string;
|
|
24
|
+
/** True when a value is a valid secp256k1 scalar (legacy short hex accepted). */
|
|
25
|
+
export declare function isValidSecp256k1PrivateKey(privateKeyHex: string): boolean;
|
|
26
|
+
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
27
|
+
export declare function isValidSecp256k1PublicKey(publicKeyHex: string): boolean;
|
|
28
|
+
/** Generate a canonical private key and its uncompressed public key. */
|
|
29
|
+
export declare function generateSecp256k1KeyPair(): Secp256k1KeyPair;
|
|
30
|
+
/** Derive a compressed or uncompressed SEC1 public key from a private scalar. */
|
|
31
|
+
export declare function deriveSecp256k1PublicKey(privateKeyHex: string, compressed?: boolean): string;
|
|
32
|
+
/** Parse and re-encode a public key in canonical lowercase SEC1 form. */
|
|
33
|
+
export declare function normalizeSecp256k1PublicKey(publicKeyHex: string, compressed?: boolean): string;
|
|
34
|
+
/**
|
|
35
|
+
* Sign a 32-byte digest with deterministic RFC 6979 ECDSA and return DER hex.
|
|
36
|
+
*
|
|
37
|
+
* `lowS` defaults to false because historical Oxy signatures used elliptic's
|
|
38
|
+
* default and therefore may occupy either half of the curve order. Verification
|
|
39
|
+
* accepts both forms; callers that require low-S normalization opt in explicitly.
|
|
40
|
+
*/
|
|
41
|
+
export declare function signSecp256k1Digest(privateKeyHex: string, digestHex: string, options?: Secp256k1SignatureOptions): string;
|
|
42
|
+
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
43
|
+
export declare function verifySecp256k1Digest(publicKeyHex: string, digestHex: string, signatureDerHex: string): boolean;
|
|
44
|
+
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
45
|
+
export declare function deriveSecp256k1SharedSecret(privateKeyHex: string, publicKeyHex: string): Uint8Array;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transparency checkpoint — the signed, hash-linked commitment to a tree root.
|
|
3
|
+
*
|
|
4
|
+
* A checkpoint is what the operator PUBLISHES: "at `periodEnd` I committed to
|
|
5
|
+
* `root` over `treeSize` subjects, and the previous checkpoint hashed to
|
|
6
|
+
* `prevCheckpointHash`". The `prevCheckpointHash` link makes the checkpoint
|
|
7
|
+
* sequence itself append-only: once any checkpoint is anchored publicly, none of
|
|
8
|
+
* its ancestors can be rewritten without breaking the chain of hashes.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the signature covers ONLY the five fields
|
|
11
|
+
*
|
|
12
|
+
* The signing input is derived from exactly `{index, periodEnd, treeSize, root,
|
|
13
|
+
* prevCheckpointHash}` — never from the surrounding storage document, and never
|
|
14
|
+
* from other signatures. That is what makes a checkpoint CO-SIGNABLE: the
|
|
15
|
+
* operator and any number of independent witnesses (e.g. user-run
|
|
16
|
+
* `@oxy.so/node` deployments) each sign the identical bytes with their own key,
|
|
17
|
+
* with zero coordination and in any order. Two conflicting roots for one
|
|
18
|
+
* `index`, each carrying valid signatures, is then transferable proof of
|
|
19
|
+
* equivocation that needs no cooperation from the operator to demonstrate.
|
|
20
|
+
*
|
|
21
|
+
* Extra fields on the object passed in are ignored by design, so handing this a
|
|
22
|
+
* database document cannot change what was signed.
|
|
23
|
+
*/
|
|
24
|
+
/** The one signature algorithm the protocol emits. */
|
|
25
|
+
declare const ALG: "ES256K-DER-SHA256";
|
|
26
|
+
/** The signed body of a checkpoint. */
|
|
27
|
+
export interface TransparencyCheckpointFields {
|
|
28
|
+
/** Monotonic checkpoint number; gapless. */
|
|
29
|
+
index: number;
|
|
30
|
+
/** End of the period this checkpoint commits to (ms epoch). */
|
|
31
|
+
periodEnd: number;
|
|
32
|
+
/** Number of leaves (subjects) committed. */
|
|
33
|
+
treeSize: number;
|
|
34
|
+
/** The Merkle root over the committed heads. */
|
|
35
|
+
root: string;
|
|
36
|
+
/** {@link checkpointHash} of the previous checkpoint; `null` at the first one. */
|
|
37
|
+
prevCheckpointHash: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** One signer's endorsement of a checkpoint — the operator's or a witness's. */
|
|
40
|
+
export interface TransparencyCheckpointSignature {
|
|
41
|
+
/** Uncompressed hex public key of the signer. */
|
|
42
|
+
publicKey: string;
|
|
43
|
+
alg: typeof ALG;
|
|
44
|
+
/** DER-encoded hex secp256k1 signature over {@link checkpointSigningInput}. */
|
|
45
|
+
signature: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The exact bytes every co-signer signs: the canonical JSON of the five signed
|
|
49
|
+
* fields under the checkpoint domain prefix. Key order in the input object is
|
|
50
|
+
* irrelevant; extra properties are dropped.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkpointSigningInput(fields: TransparencyCheckpointFields): string;
|
|
53
|
+
/**
|
|
54
|
+
* The content address of a checkpoint — what the NEXT checkpoint's
|
|
55
|
+
* `prevCheckpointHash` references.
|
|
56
|
+
*/
|
|
57
|
+
export declare function checkpointHash(fields: TransparencyCheckpointFields): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Sign a checkpoint with an explicit private key. Used by the operator and,
|
|
60
|
+
* identically, by every witness that co-signs the same checkpoint.
|
|
61
|
+
*/
|
|
62
|
+
export declare function signCheckpoint(fields: TransparencyCheckpointFields, privateKeyHex: string): Promise<TransparencyCheckpointSignature>;
|
|
63
|
+
/**
|
|
64
|
+
* Verify one signature over a checkpoint's signed fields.
|
|
65
|
+
*
|
|
66
|
+
* Confirms the signature matches the embedded `publicKey` for exactly these
|
|
67
|
+
* fields. It does NOT establish that the key belongs to a trusted operator or an
|
|
68
|
+
* accepted witness — that policy lives with the verifier's key list.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyCheckpointSignature(fields: TransparencyCheckpointFields, signature: TransparencyCheckpointSignature): Promise<boolean>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transparency Merkle tree — leaf hashing, root computation, and inclusion
|
|
3
|
+
* proofs over a snapshot of chain heads.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is for
|
|
6
|
+
*
|
|
7
|
+
* A per-subject hash chain proves nobody edited a record. It does NOT prove the
|
|
8
|
+
* SERVER didn't serve two different histories to two parties (equivocation), or
|
|
9
|
+
* quietly drop a record. That gap is closed by committing to every subject's
|
|
10
|
+
* chain head at once, publishing the commitment, and letting anyone verify that
|
|
11
|
+
* their own head is inside it — which is exactly what this tree does. The
|
|
12
|
+
* commitment (the root) is what gets signed into a checkpoint (`./checkpoint`)
|
|
13
|
+
* and anchored on a public chain.
|
|
14
|
+
*
|
|
15
|
+
* ## Shape
|
|
16
|
+
*
|
|
17
|
+
* The tree follows RFC 6962 (Certificate Transparency) so the algorithms are
|
|
18
|
+
* standard and independently reimplementable: leaves and interior nodes are
|
|
19
|
+
* hashed under DISTINCT domain prefixes (a leaf hash can never be reinterpreted
|
|
20
|
+
* as an interior node), an odd level splits so the LEFT subtree is the largest
|
|
21
|
+
* power of two below the size, and a single-leaf tree's root is the leaf itself.
|
|
22
|
+
*
|
|
23
|
+
* A built tree keeps every LEVEL, not just its leaves, because that is what
|
|
24
|
+
* makes serving proofs cheap: {@link inclusionProof} reads siblings straight out
|
|
25
|
+
* of the levels instead of re-hashing subtrees, so one build amortizes over all
|
|
26
|
+
* the proofs cut from it (a checkpoint is built once and proved thousands of
|
|
27
|
+
* times, once per subject that audits it).
|
|
28
|
+
*
|
|
29
|
+
* Crucially, {@link verifyInclusionProof} needs only the verifier's OWN leaf,
|
|
30
|
+
* its index, the tree size, the audit path, and the signed root — never the
|
|
31
|
+
* other leaves. So a device or node can audit its own history against a
|
|
32
|
+
* published checkpoint without downloading anyone else's data.
|
|
33
|
+
*
|
|
34
|
+
* The leaf order is fixed by {@link buildTransparencyTreeFromHeads}: ascending
|
|
35
|
+
* by `subjectDid` in UTF-16 code-unit order. Order is part of the commitment, so
|
|
36
|
+
* every implementation MUST sort identically or the roots diverge.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* The root of a tree with zero leaves: `sha256("oxy.transparency.empty.v1")`.
|
|
40
|
+
*
|
|
41
|
+
* Hard-coded because hashing is async and this is needed as a value; a Jest
|
|
42
|
+
* regression test pins it against the live hash of {@link EMPTY_PREIMAGE}.
|
|
43
|
+
*/
|
|
44
|
+
export declare const EMPTY_TRANSPARENCY_ROOT = "315338df4bc34de7d057b583a082268016888323c79c0376586406a64f441b1e";
|
|
45
|
+
/**
|
|
46
|
+
* One subject's committed chain position: the head of their signed-record chain
|
|
47
|
+
* at the moment the snapshot was taken. Mirrors the `RepoHead` row an app keeps.
|
|
48
|
+
*/
|
|
49
|
+
export interface TransparencyHeadEntry {
|
|
50
|
+
/** The chain's subject DID (`did:web:<domain>:u:<userId>`). */
|
|
51
|
+
subjectDid: string;
|
|
52
|
+
/** The `seq` of the head record. */
|
|
53
|
+
seq: number;
|
|
54
|
+
/** The `recordId` (content address) of the head record. */
|
|
55
|
+
headRecordId: string;
|
|
56
|
+
}
|
|
57
|
+
/** A built tree: its commitment plus every level proofs are cut from. */
|
|
58
|
+
export interface TransparencyTree {
|
|
59
|
+
root: string;
|
|
60
|
+
treeSize: number;
|
|
61
|
+
/**
|
|
62
|
+
* Every level bottom-up: `levels[0]` is the leaf hashes in committed order,
|
|
63
|
+
* each next level is the one below it paired up, and the last is `[root]`
|
|
64
|
+
* (empty for a zero-leaf tree).
|
|
65
|
+
*/
|
|
66
|
+
levels: string[][];
|
|
67
|
+
}
|
|
68
|
+
/** A tree built from head entries, with the leaf index of every subject. */
|
|
69
|
+
export interface TransparencyTreeFromHeads extends TransparencyTree {
|
|
70
|
+
/** `subjectDid` → leaf index, so a proof can be served by subject. */
|
|
71
|
+
indexBySubject: Record<string, number>;
|
|
72
|
+
}
|
|
73
|
+
/** Arguments for {@link verifyInclusionProof}. */
|
|
74
|
+
export interface InclusionProofCheck {
|
|
75
|
+
/** The verifier's own leaf hash ({@link transparencyLeafHash}). */
|
|
76
|
+
leaf: string;
|
|
77
|
+
/** The leaf's index in the committed order. */
|
|
78
|
+
index: number;
|
|
79
|
+
/** The `treeSize` the checkpoint committed to. */
|
|
80
|
+
treeSize: number;
|
|
81
|
+
/** The audit path, leaf-adjacent sibling first. */
|
|
82
|
+
proof: string[];
|
|
83
|
+
/** The root the checkpoint committed to. */
|
|
84
|
+
root: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Hash one subject's head into a leaf.
|
|
88
|
+
*
|
|
89
|
+
* The pre-image is the canonical JSON of the three committed fields under the
|
|
90
|
+
* leaf domain prefix — so a DID containing a delimiter-like character cannot
|
|
91
|
+
* forge another subject's leaf (JSON escaping makes the encoding unambiguous),
|
|
92
|
+
* and a leaf can never collide with an interior node.
|
|
93
|
+
*/
|
|
94
|
+
export declare function transparencyLeafHash(entry: TransparencyHeadEntry): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Build a tree over already-hashed leaves, in the given order.
|
|
97
|
+
*
|
|
98
|
+
* Levels are built bottom-up, each one pairing the level below and carrying a
|
|
99
|
+
* trailing odd node up unchanged — which yields exactly RFC 6962's tree, whose
|
|
100
|
+
* recursive definition splits at the largest power of two below the size.
|
|
101
|
+
* `transparency.test.ts` pins that equivalence against a direct transcription of
|
|
102
|
+
* the RFC's MTH for every size up to 40, so the shape can never silently drift.
|
|
103
|
+
*
|
|
104
|
+
* The order IS part of the commitment — prefer
|
|
105
|
+
* {@link buildTransparencyTreeFromHeads}, which owns the canonical ordering.
|
|
106
|
+
*/
|
|
107
|
+
export declare function buildTransparencyTree(leaves: string[]): Promise<TransparencyTree>;
|
|
108
|
+
/**
|
|
109
|
+
* Build the canonical tree for a snapshot of chain heads.
|
|
110
|
+
*
|
|
111
|
+
* Sorts ascending by `subjectDid` in UTF-16 code-unit order — NEVER
|
|
112
|
+
* `localeCompare`, whose order is locale-dependent and would make two
|
|
113
|
+
* verifiers compute different roots from identical data.
|
|
114
|
+
*
|
|
115
|
+
* Throws on a duplicate `subjectDid`: a snapshot must commit to exactly one head
|
|
116
|
+
* per subject, and silently keeping one of two would hide the other's history.
|
|
117
|
+
*/
|
|
118
|
+
export declare function buildTransparencyTreeFromHeads(entries: TransparencyHeadEntry[]): Promise<TransparencyTreeFromHeads>;
|
|
119
|
+
/**
|
|
120
|
+
* The audit path proving `index` is committed in `tree` (RFC 6962 PATH): each
|
|
121
|
+
* step is the sibling subtree hash, leaf-adjacent first.
|
|
122
|
+
*
|
|
123
|
+
* Pure index arithmetic over the tree's levels — no hashing, so cutting a proof
|
|
124
|
+
* costs O(log n) array reads however large the checkpoint is.
|
|
125
|
+
*/
|
|
126
|
+
export declare function inclusionProof(tree: TransparencyTree, index: number): string[];
|
|
127
|
+
/**
|
|
128
|
+
* Verify an audit path against a committed root (RFC 6962 §2.1.1).
|
|
129
|
+
*
|
|
130
|
+
* Recomputes the root from the leaf upward using only the path, so the verifier
|
|
131
|
+
* never needs another subject's data. Returns `false` for every failure mode —
|
|
132
|
+
* tampered leaf, replayed index, truncated path, foreign root — rather than
|
|
133
|
+
* throwing, so callers treat auditing as a boolean.
|
|
134
|
+
*/
|
|
135
|
+
export declare function verifyInclusionProof(check: InclusionProofCheck): Promise<boolean>;
|
package/package.json
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oxy.so/protocol",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Oxy Protocol — the app-agnostic base substrate: signed-record envelope, canonical JSON, signature/verification, and platform crypto. Reused by any Oxy app to decentralize its own content.",
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"secp256k1": [
|
|
11
|
+
"dist/types/secp256k1.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"node": [
|
|
14
|
+
"dist/types/node/index.d.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"source": "src/index.ts",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"react-native": "./dist/esm/index.js",
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
28
|
+
"default": "./dist/esm/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
32
|
+
"default": "./dist/cjs/index.js"
|
|
33
|
+
},
|
|
34
|
+
"default": "./dist/esm/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./node": {
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./dist/types/node/index.d.ts",
|
|
39
|
+
"default": "./dist/esm/node/index.js"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"types": "./dist/types/node/index.d.ts",
|
|
43
|
+
"default": "./dist/cjs/node/index.js"
|
|
44
|
+
},
|
|
45
|
+
"default": "./dist/esm/node/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./secp256k1": {
|
|
48
|
+
"react-native": "./dist/esm/secp256k1.js",
|
|
49
|
+
"import": {
|
|
50
|
+
"types": "./dist/types/secp256k1.d.ts",
|
|
51
|
+
"default": "./dist/esm/secp256k1.js"
|
|
52
|
+
},
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/types/secp256k1.d.ts",
|
|
55
|
+
"default": "./dist/cjs/secp256k1.js"
|
|
56
|
+
},
|
|
57
|
+
"default": "./dist/esm/secp256k1.js"
|
|
58
|
+
},
|
|
59
|
+
"./package.json": "./package.json"
|
|
60
|
+
},
|
|
61
|
+
"react-native": {
|
|
62
|
+
"./dist/esm/platform/crypto.js": "./dist/esm/platform/crypto.native.js",
|
|
63
|
+
"./dist/cjs/platform/crypto.js": "./dist/cjs/platform/crypto.native.js"
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"NOTICE",
|
|
67
|
+
"dist",
|
|
68
|
+
"src"
|
|
69
|
+
],
|
|
70
|
+
"keywords": [
|
|
71
|
+
"oxyhq",
|
|
72
|
+
"protocol",
|
|
73
|
+
"signed-records",
|
|
74
|
+
"canonical-json",
|
|
75
|
+
"crypto",
|
|
76
|
+
"did"
|
|
77
|
+
],
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "https://github.com/oxyhq/sdk",
|
|
81
|
+
"directory": "packages/protocol"
|
|
82
|
+
},
|
|
83
|
+
"author": "OxyHQ",
|
|
84
|
+
"license": "Apache-2.0",
|
|
85
|
+
"homepage": "https://oxy.so",
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=18.0.0"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "bun run build:cjs && bun run build:esm && bun run build:types",
|
|
91
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
92
|
+
"build:esm": "tsc -p tsconfig.esm.json && node scripts/fix-esm-imports.mjs",
|
|
93
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
94
|
+
"clean": "rm -rf dist",
|
|
95
|
+
"typescript": "tsc --noEmit",
|
|
96
|
+
"test": "jest --passWithNoTests",
|
|
97
|
+
"lint": "biome lint --error-on-warnings ./src",
|
|
98
|
+
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run clean && bun run build",
|
|
99
|
+
"release": "rm -rf dist && bun run build && release-it"
|
|
100
|
+
},
|
|
101
|
+
"release-it": {
|
|
102
|
+
"git": {
|
|
103
|
+
"tagName": "@oxy.so/protocol@${version}",
|
|
104
|
+
"tagAnnotation": "Release @oxy.so/protocol@${version}",
|
|
105
|
+
"commitMessage": "chore(protocol): release @oxy.so/protocol@${version}"
|
|
106
|
+
},
|
|
107
|
+
"github": {
|
|
108
|
+
"release": true,
|
|
109
|
+
"releaseName": "@oxy.so/protocol@${version}"
|
|
110
|
+
},
|
|
111
|
+
"npm": {
|
|
112
|
+
"publish": true
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"dependencies": {
|
|
116
|
+
"@noble/curves": "^1.9.7",
|
|
117
|
+
"@oxy.so/contracts": "^1.0.0",
|
|
118
|
+
"zod": "^3.25.64"
|
|
119
|
+
},
|
|
120
|
+
"peerDependencies": {
|
|
121
|
+
"@react-native-async-storage/async-storage": "*",
|
|
122
|
+
"expo-crypto": "*",
|
|
123
|
+
"expo-secure-store": "*",
|
|
124
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
125
|
+
"expo-modules-core": "*"
|
|
126
|
+
},
|
|
127
|
+
"peerDependenciesMeta": {
|
|
128
|
+
"@react-native-async-storage/async-storage": {
|
|
129
|
+
"optional": true
|
|
130
|
+
},
|
|
131
|
+
"expo-crypto": {
|
|
132
|
+
"optional": true
|
|
133
|
+
},
|
|
134
|
+
"expo-secure-store": {
|
|
135
|
+
"optional": true
|
|
136
|
+
},
|
|
137
|
+
"express": {
|
|
138
|
+
"optional": true
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"devDependencies": {
|
|
142
|
+
"@biomejs/biome": "^1.9.4",
|
|
143
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
144
|
+
"@types/express": "^4.17.25",
|
|
145
|
+
"@types/node": "^22.20.1",
|
|
146
|
+
"@types/supertest": "^6.0.2",
|
|
147
|
+
"expo-crypto": "~56.0.3",
|
|
148
|
+
"expo-modules-core": "~57.0.17",
|
|
149
|
+
"expo-secure-store": "~56.0.4",
|
|
150
|
+
"express": "^4.22.2",
|
|
151
|
+
"jest": "^30.5.1",
|
|
152
|
+
"release-it": "^19.0.6",
|
|
153
|
+
"supertest": "^7.0.0",
|
|
154
|
+
"ts-jest": "^29.4.12",
|
|
155
|
+
"typescript": "^5.9.2"
|
|
156
|
+
}
|
|
157
|
+
}
|