Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
40cdd35
wip [skip ci]
mabels Aug 4, 2025
a8f5b25
wip
mabels Aug 7, 2025
c7ea1bb
wip: ctx switch
mabels Aug 14, 2025
c8aa4bf
test: add subscription tests for database attach and sync operations
jchris Aug 21, 2025
6802ebe
test: add detailed bug reproduction tests for remote sync subscriptio…
jchris Aug 21, 2025
bb8a016
fix: trigger subscriptions when remote data syncs in via CRDT clock u…
jchris Aug 21, 2025
1644ad3
test: improve subscription test assertions and remove console logging
jchris Aug 21, 2025
9afb72a
fix: update type signature for receivedDocs Map to use Record<string,…
jchris Aug 21, 2025
39050cf
test: update receivedDocs type to use DocBase for subscription tracking
jchris Aug 21, 2025
24b4490
refactor: add type annotation to get() call in subscription test
jchris Aug 21, 2025
17909ea
fix: ensure subscription callbacks fire reliably during sync operations
jchris Aug 21, 2025
0b48b6f
test: simplify sync test to use allDocs instead of individual gets
jchris Aug 21, 2025
de13f35
fix: ensure subscription triggers for empty watchers and relax test a…
jchris Aug 22, 2025
51e46ec
feat: add debug logging to trace subscription notification paths
jchris Aug 22, 2025
8929a76
refactor: rename emptyWatchers to noPayloadWatchers for clarity
jchris Aug 22, 2025
24d46db
refactor: rename emptyWatchers to noPayloadWatchers for clarity in de…
jchris Aug 22, 2025
40a4e6b
feat: add detailed logging for CRDT head changes and watcher notifica…
jchris Aug 22, 2025
d65d37c
refactor: replace Promise.all map with for loop in writeRow test helper
jchris Aug 22, 2025
66e04e4
refactor: replace console.log statements with structured logger calls…
jchris Aug 22, 2025
72c073a
refactor: remove unused headChanged variable in CRDT clock implementa…
jchris Aug 22, 2025
14b164a
feat: add database name to CRDTClock logger context
jchris Aug 22, 2025
d587da3
refactor: format logger initialization with line breaks for readability
jchris Aug 22, 2025
f5b3ef5
docs: add CRDTClock to debug pattern example in test instructions
jchris Aug 22, 2025
4df7584
refactor: simplify logging by removing emojis and standardizing strin…
jchris Aug 22, 2025
fb4f2ea
style: standardize string delimiter usage in CRDT logging
jchris Aug 22, 2025
5c88be6
test: reduce test data size in attachable subscription tests
jchris Aug 22, 2025
0c984c5
wip [skip ci]
mabels Aug 22, 2025
6b7bcb4
wip [skip ci]
mabels Aug 26, 2025
7cbde6f
Add JWKS environment string converter
Vaeshkar Aug 21, 2025
9d32b44
Add JWKS validator module with tests
Vaeshkar Aug 26, 2025
bd729dc
feat: add JWKS validator with comprehensive error handling and browse…
Vaeshkar Aug 26, 2025
751f0ba
clean up and validator nitpicks
Vaeshkar Aug 26, 2025
c1b3c20
import mistake
Vaeshkar Aug 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ logs
**/*.tgz
*.zip
*.tgz
.npmrc

smoke/package.json
smoke/pnpm-lock.yaml
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ pnpm run test -t 'test name pattern' path/to/test/file
For example, to run a specific test for the CRDT module, in just one project:

```bash
FP_DEBUG=Loader pnpm run test --project file -t 'codec implict iv' crdt
FP_DEBUG='Loader,CRDTClock' pnpm run test --project file -t 'codec implict iv' crdt
```

For testing React components, you can use:
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/core-runtime": "workspace:0.0.0",
"@fireproof/core-types-base": "workspace:0.0.0",
"@fireproof/vendor": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cloud/3rd-party/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"react-dom": "^19.1.1",
"use-fireproof": "workspace:0.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion cloud/backend/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@cloudflare/workers-types": "^4.20250810.0",
"@fireproof/cloud-base": "workspace:0.0.0",
"@fireproof/core-base": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cloud/backend/base/test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export async function mockJWK(sthis: SuperThis, claim: Partial<TokenForParam> =
token: keys.strings.privateKey,
});

const id = claim.id ?? sthis.nextId().str;
const id = claim.jti ?? sthis.nextId().str;
const claims: ps.TokenForParam = {
userId: `hello-${id}`,
email: `hello-${id}@test.de`,
Expand Down
2 changes: 1 addition & 1 deletion cloud/backend/cf-d1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@cloudflare/workers-types": "^4.20250810.0",
"@fireproof/cloud-backend-base": "workspace:0.0.0",
"@fireproof/cloud-base": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cloud/backend/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/cloud-backend-base": "workspace:0.0.0",
"@fireproof/cloud-base": "workspace:0.0.0",
"@fireproof/core-base": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cloud/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"react": ">=18.0.0"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/core-blockstore": "workspace:0.0.0",
"@fireproof/core-runtime": "workspace:0.0.0",
"@fireproof/core-types-base": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cloud/todo-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"react": ">=18.0.0"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/vendor": "workspace:0.0.0",
"@types/react": "^19.1.8",
"react-dom": "^19.1.0",
Expand Down
59 changes: 53 additions & 6 deletions core/base/crdt-clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class CRDTClockImpl {

readonly zoomers = new Map<string, VoidFn>();
readonly watchers = new Map<string, (updates: DocUpdate<DocTypes>[]) => void>();
readonly emptyWatchers = new Map<string, VoidFn>();
readonly noPayloadWatchers = new Map<string, VoidFn>();

readonly blockstore: BaseBlockstore; // ready blockstore

Expand All @@ -46,7 +46,10 @@ export class CRDTClockImpl {
constructor(blockstore: BaseBlockstore) {
this.sthis = blockstore.sthis;
this.blockstore = blockstore;
this.logger = ensureLogger(blockstore.sthis, "CRDTClock");
this.logger = ensureLogger(blockstore.sthis, `CRDTClock`)
.With()
.Str("dbName", blockstore.crdtParent?.ledgerParent?.name || "unnamed")
.Logger();
this.applyHeadQueue = applyHeadQueue(this.int_applyHead.bind(this), this.logger);
}

Expand Down Expand Up @@ -80,7 +83,15 @@ export class CRDTClockImpl {
if (!updates.length) {
return;
}
this.emptyWatchers.forEach((fn) => fn());
this.logger
.Debug()
.Int("updatesCount", updates.length)
.Int("watchersCount", this.watchers.size)
.Int("noPayloadWatchersCount", this.noPayloadWatchers.size)
.Msg("NOTIFY_WATCHERS: Triggering subscriptions");
// Always notify both types of watchers - subscription systems need notifications
// regardless of whether there are document updates
this.noPayloadWatchers.forEach((fn) => fn());
this.watchers.forEach((fn) => fn(updates || []));
}

Expand All @@ -94,9 +105,9 @@ export class CRDTClockImpl {

onTock(fn: VoidFn): UnReg {
const key = this.sthis.timeOrderedNextId().str;
this.emptyWatchers.set(key, fn);
this.noPayloadWatchers.set(key, fn);
return () => {
this.emptyWatchers.delete(key);
this.noPayloadWatchers.delete(key);
};
}

Expand All @@ -114,7 +125,18 @@ export class CRDTClockImpl {
// }

const noLoader = !localUpdates;

const needsManualNotification = !localUpdates && (this.watchers.size > 0 || this.noPayloadWatchers.size > 0);

this.logger
.Debug()
.Bool("localUpdates", localUpdates)
.Int("watchersCount", this.watchers.size)
.Int("noPayloadWatchersCount", this.noPayloadWatchers.size)
.Bool("needsManualNotification", needsManualNotification)
.Int("headLength", newHead.length)
.Int("prevHeadLength", prevHead.length)
.Int("currentHeadLength", this.head.length)
.Msg("INT_APPLY_HEAD: Entry point");
// console.log("int_applyHead", this.applyHeadQueue.size(), this.head, newHead, prevHead, localUpdates);
const ogHead = sortClockHead(this.head);
newHead = sortClockHead(newHead);
Expand Down Expand Up @@ -156,6 +178,31 @@ export class CRDTClockImpl {
this.transaction = undefined;
}
this.setHead(advancedHead);

if (needsManualNotification) {
const changes = await clockChangesSince<DocTypes>(this.blockstore, advancedHead, prevHead, {}, this.logger);
const triggerReason =
this.watchers.size > 0 && this.noPayloadWatchers.size > 0
? "both"
: this.watchers.size > 0
? "watchers"
: "noPayloadWatchers";
this.logger
.Debug()
.Int("changesCount", changes.result.length)
.Str("triggerReason", triggerReason)
.Int("watchersCount", this.watchers.size)
.Int("noPayloadWatchersCount", this.noPayloadWatchers.size)
.Msg("MANUAL_NOTIFICATION: Checking for changes");
if (changes.result.length > 0) {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling notifyWatchers with changes");
this.notifyWatchers(changes.result);
this.noPayloadWatchers.forEach((fn) => fn());
} else {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling noPayloadWatchers directly");
this.noPayloadWatchers.forEach((fn) => fn());
}
}
Comment on lines +182 to +205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid duplicate noPayloadWatcher invocations when changes > 0.

In the manual path you call notifyWatchers(changes.result), which already triggers noPayloadWatchers, and then you invoke noPayloadWatchers again — leading to duplicate tock callbacks.

       if (changes.result.length > 0) {
         this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling notifyWatchers with changes");
-        this.notifyWatchers(changes.result);
-        this.noPayloadWatchers.forEach((fn) => fn());
+        this.notifyWatchers(changes.result);
       } else {
         this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling noPayloadWatchers directly");
         this.noPayloadWatchers.forEach((fn) => fn());
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (needsManualNotification) {
const changes = await clockChangesSince<DocTypes>(this.blockstore, advancedHead, prevHead, {}, this.logger);
const triggerReason =
this.watchers.size > 0 && this.noPayloadWatchers.size > 0
? "both"
: this.watchers.size > 0
? "watchers"
: "noPayloadWatchers";
this.logger
.Debug()
.Int("changesCount", changes.result.length)
.Str("triggerReason", triggerReason)
.Int("watchersCount", this.watchers.size)
.Int("noPayloadWatchersCount", this.noPayloadWatchers.size)
.Msg("MANUAL_NOTIFICATION: Checking for changes");
if (changes.result.length > 0) {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling notifyWatchers with changes");
this.notifyWatchers(changes.result);
this.noPayloadWatchers.forEach((fn) => fn());
} else {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling noPayloadWatchers directly");
this.noPayloadWatchers.forEach((fn) => fn());
}
}
if (needsManualNotification) {
const changes = await clockChangesSince<DocTypes>(this.blockstore, advancedHead, prevHead, {}, this.logger);
const triggerReason =
this.watchers.size > 0 && this.noPayloadWatchers.size > 0
? "both"
: this.watchers.size > 0
? "watchers"
: "noPayloadWatchers";
this.logger
.Debug()
.Int("changesCount", changes.result.length)
.Str("triggerReason", triggerReason)
.Int("watchersCount", this.watchers.size)
.Int("noPayloadWatchersCount", this.noPayloadWatchers.size)
.Msg("MANUAL_NOTIFICATION: Checking for changes");
if (changes.result.length > 0) {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling notifyWatchers with changes");
this.notifyWatchers(changes.result);
} else {
this.logger.Debug().Msg("MANUAL_NOTIFICATION: Calling noPayloadWatchers directly");
this.noPayloadWatchers.forEach((fn) => fn());
}
}
🤖 Prompt for AI Agents
In core/base/crdt-clock.ts around lines 182 to 205, the manual-notification
branch calls notifyWatchers(changes.result) which already triggers the
noPayloadWatchers, and then calls noPayloadWatchers.forEach(...) again causing
duplicate invocations; remove the second noPayloadWatchers.forEach(...) from the
branch where changes.result.length > 0 so that noPayloadWatchers are only
invoked once (keep the noPayloadWatchers.forEach(...) in the else branch for the
no-changes case).

}
}

Expand Down
17 changes: 17 additions & 0 deletions core/base/crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ export class CRDTImpl implements CRDT {
const crdtMeta = meta as CRDTMeta;
if (!crdtMeta.head) throw this.logger.Error().Msg("missing head").AsError();
// console.log("applyMeta-pre", crdtMeta.head, this.clock.head);
this.logger
.Debug()
.Str("newHead", crdtMeta.head.map((h) => h.toString()).join(","))
.Int("subscribers", this.clock.watchers.size + this.clock.noPayloadWatchers.size)
.Int("headLength", crdtMeta.head.length)
.Int("currentHeadLength", this.clock.head.length)
.Str("dbName", this.opts.name || "unnamed")
.Msg("APPLY_META: Calling applyHead for REMOTE sync");
await this.clock.applyHead(crdtMeta.head, []);
Comment on lines +109 to 117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Pass the current head as prevHead in applyMeta; avoid using an empty prevHead.

Using [] as prevHead causes downstream code (clockChangesSince) to treat the change as “since genesis,” potentially flooding subscribers with all historical updates on each remote sync. Pass a snapshot of the current head instead.

-        await this.clock.applyHead(crdtMeta.head, []);
+        const prevHead = [...this.clock.head];
+        await this.clock.applyHead(crdtMeta.head, prevHead);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.logger
.Debug()
.Str("newHead", crdtMeta.head.map((h) => h.toString()).join(","))
.Int("subscribers", this.clock.watchers.size + this.clock.noPayloadWatchers.size)
.Int("headLength", crdtMeta.head.length)
.Int("currentHeadLength", this.clock.head.length)
.Str("dbName", this.opts.name || "unnamed")
.Msg("APPLY_META: Calling applyHead for REMOTE sync");
await this.clock.applyHead(crdtMeta.head, []);
this.logger
.Debug()
.Str("newHead", crdtMeta.head.map((h) => h.toString()).join(","))
.Int("subscribers", this.clock.watchers.size + this.clock.noPayloadWatchers.size)
.Int("headLength", crdtMeta.head.length)
.Int("currentHeadLength", this.clock.head.length)
.Str("dbName", this.opts.name || "unnamed")
.Msg("APPLY_META: Calling applyHead for REMOTE sync");
const prevHead = [...this.clock.head];
await this.clock.applyHead(crdtMeta.head, prevHead);
🤖 Prompt for AI Agents
In core/base/crdt.ts around lines 109 to 117, the call to this.clock.applyHead
uses an empty array for prevHead which causes downstream code to treat the
change as "since genesis" and flood subscribers; replace the empty prevHead with
a snapshot of the current head (e.g., a shallow copy of this.clock.head) so
applyHead receives the actual previous head state, ensuring clockChangesSince
computes diffs correctly and avoids replaying the entire history.

// console.log("applyMeta-post", crdtMeta.head, this.clock.head);
},
Expand Down Expand Up @@ -184,6 +192,15 @@ export class CRDTImpl implements CRDT {
});
return { head };
});
this.logger
.Debug()
.Str("newHead", done.meta.head.map((h) => h.toString()).join(","))
.Int("subscribers", this.clock.watchers.size + this.clock.noPayloadWatchers.size)
.Int("headLength", done.meta.head.length)
.Int("prevHeadLength", prevHead.length)
.Int("currentHeadLength", this.clock.head.length)
.Str("dbName", this.opts.name || "unnamed")
.Msg("BULK: Calling applyHead for LOCAL write");
await this.clock.applyHead(done.meta.head, prevHead, updates);
return done.meta;
}
Expand Down
2 changes: 1 addition & 1 deletion core/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify repo-wide version alignment and lockfile refresh.

Ensure all packages (deps, devDeps, peerDeps) consistently reference @adviser/cement ^0.4.26 and refresh the workspace lockfile to avoid mixed resolutions.

Run this script from repo root to surface any mismatches (e.g., deps vs peerDeps):


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo -e "File\tdep\tdevDep\tpeerDep"
fd -a -H package.json | while read -r f; do
  jq -r --arg PKG "@adviser/cement" '
    . as $root
    | [
        input_filename,
        ($root.dependencies[$PKG] // ""),
        ($root.devDependencies[$PKG] // ""),
        ($root.peerDependencies[$PKG] // "")
      ] | @tsv
  ' "$f" 2>/dev/null
done | sort

echo
echo "Mismatches (dependency != peerDependency):"
fd -a -H package.json | while read -r f; do
  jq -r --arg PKG "@adviser/cement" '
    . as $root
    | ($root.dependencies[$PKG] // "") as $dep
    | ($root.peerDependencies[$PKG] // "") as $peer
    | if ($dep != "" and $peer != "" and $dep != $peer) then
        (input_filename + "\tdep=" + $dep + "\tpeer=" + $peer)
      else empty end
  ' "$f" 2>/dev/null
done

Length of output: 11352


Align @adviser/cement versions across all packages & refresh lockfile

It looks like two consumer packages are still on older versions of @adviser/cement, which will lead to mixed resolutions in the workspace. Please update them to ^0.4.26 and then regenerate the lockfile.

Files to update:

  • core/jwks/package.json (currently ^0.4.25)
  • examples/react-router/package.json (currently ^0.4.21)

Suggested diffs:

--- core/jwks/package.json
+++ core/jwks/package.json
@@ -12,7 +12,7 @@
   "dependencies": {
-    "@adviser/cement": "^0.4.25",
+    "@adviser/cement": "^0.4.26",
     /* … */
   }
 }
--- examples/react-router/package.json
+++ examples/react-router/package.json
@@ -8,7 +8,7 @@
   "dependencies": {
-    "@adviser/cement": "^0.4.21",
+    "@adviser/cement": "^0.4.26",
     /* … */
   }
 }

Once these are updated, run your package manager’s install command (e.g. yarn install or npm install) from the repo root to refresh the lockfile and ensure a single, consistent resolution of @adviser/cement across the monorepo.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@adviser/cement": "^0.4.26",
--- core/jwks/package.json
@@ -12,7 +12,7 @@
"dependencies": {
"@adviser/cement": "^0.4.26",
/* … */
}
}
Suggested change
"@adviser/cement": "^0.4.26",
--- examples/react-router/package.json
@@ -8,7 +8,7 @@
"dependencies": {
"@adviser/cement": "^0.4.26",
/* … */
}
}
🤖 Prompt for AI Agents
In core/base/package.json around line 39, the workspace is mixing
@adviser/cement versions; update the consumers core/jwks/package.json and
examples/react-router/package.json to use "^0.4.26" (replacing "^0.4.25" and
"^0.4.21" respectively) and then run your package manager install (e.g. yarn
install or npm install) from the repo root to regenerate the lockfile so the
workspace resolves a single consistent @adviser/cement version.

"@fireproof/core-blockstore": "workspace:0.0.0",
"@fireproof/core-keybag": "workspace:0.0.0",
"@fireproof/core-runtime": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion core/blockstore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"url": "https://github.com/fireproof-storage/fireproof/issues"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/core-gateways-base": "workspace:0.0.0",
"@fireproof/core-gateways-cloud": "workspace:0.0.0",
"@fireproof/core-gateways-file": "workspace:0.0.0",
Expand Down
2 changes: 1 addition & 1 deletion core/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"react": ">=18.0.0"
},
"dependencies": {
"@adviser/cement": "^0.4.25",
"@adviser/cement": "^0.4.26",
"@fireproof/core-base": "workspace:0.0.0",
"@fireproof/core-types-base": "workspace:0.0.0",
"@fireproof/vendor": "workspace:0.0.0",
Expand Down
63 changes: 63 additions & 0 deletions core/device-id/certor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { toSortedObject } from "@adviser/cement/utils";
import { Base64EndeCoder } from "@fireproof/core-types-base";
import { decodeJwt } from "jose";
import { base58btc } from "multiformats/bases/base58";
import { sha1 } from "multiformats/hashes/sha1";
import { sha256 } from "multiformats/hashes/sha2";
import { CertificatePayload, CertificatePayloadSchema } from "@fireproof/core-types-base/fp-ca-cert-payload.zod.js";

export class Certor {
readonly #cert: CertificatePayload;
readonly base64: Base64EndeCoder;
#strCert?: string;
#uint8Cert?: Uint8Array;

static fromString(base64: Base64EndeCoder, cert: string) {
const certObj = CertificatePayloadSchema.parse(JSON.parse(base64.decode(cert)));
return new Certor(base64, certObj);
}

static fromJWT(base64: Base64EndeCoder, jwtString: string) {
// const header = decodeProtectedHeader(jwtString);
const payload = decodeJwt(jwtString);
const certObj = CertificatePayloadSchema.parse(payload);
return new Certor(base64, certObj);
}

constructor(base64: Base64EndeCoder, cert: CertificatePayload) {
this.#cert = cert;
this.base64 = base64;
}

asCert(): CertificatePayload {
return this.#cert;
}

parseCertificateSubject(s: string): Record<string, string> {
const parts: Record<string, string> = {};
s.split(",").forEach((part) => {
const [key, value] = part.trim().split("=");
if (key && value) {
parts[key] = value;
}
});
return parts;
}

async asSHA1() {
this.#uint8Cert ||= this.base64.decodeUint8(this.asBase64());
const val = await sha1.digest(this.#uint8Cert);
return base58btc.encode(val.bytes);
}

async asSHA256() {
this.#uint8Cert ||= this.base64.decodeUint8(this.asBase64());
const val = await sha256.digest(this.#uint8Cert);
return base58btc.encode(val.bytes);
}

asBase64() {
this.#strCert ||= this.base64.encode(JSON.stringify(toSortedObject(this.#cert)));
return this.#strCert;
}
}
Loading