@c7-digital/ledger 0.0.3 → 0.0.4

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.
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from "eventemitter3";
2
2
  import { logger } from "./logger.js";
3
- import { TemplateEmitterMap } from "./TemplateEmitterMap.js";
3
+ import { PackageIdEmitterMap, } from "./PackageIdEmitterMap.js";
4
4
  /**
5
5
  * Adapts a Stream instance to the MultiStream interface
6
6
  * Provides template-specific event handlers with proper typing
@@ -11,7 +11,7 @@ export class MultiStreamAdapter {
11
11
  * @param stream The underlying Stream instance
12
12
  */
13
13
  constructor(stream) {
14
- this.templateEmitters = new TemplateEmitterMap();
14
+ this.packageIdEmitters = new PackageIdEmitterMap();
15
15
  this.errorEmitter = new EventEmitter(); // Dedicated error emitter
16
16
  this.stateEmitter = new EventEmitter(); // Dedicated state emitter
17
17
  this.stream = stream;
@@ -22,38 +22,38 @@ export class MultiStreamAdapter {
22
22
  this.stream.on("state", this.handleStateEvent.bind(this));
23
23
  }
24
24
  /**
25
- * Get or create an EventEmitter for a specific template
25
+ * Get or create an EventEmitter for a specific package ID
26
26
  */
27
- getEmitterForTemplate(templateId) {
28
- if (!this.templateEmitters.has(templateId)) {
29
- this.templateEmitters.set(templateId, new EventEmitter());
27
+ getEmitterForPackageId(packageId) {
28
+ if (!this.packageIdEmitters.has(packageId)) {
29
+ this.packageIdEmitters.set(packageId, new EventEmitter());
30
30
  }
31
- return this.templateEmitters.get(templateId);
31
+ return this.packageIdEmitters.get(packageId);
32
32
  }
33
33
  /**
34
34
  * Handle create events from the underlying stream
35
- * and route them to the appropriate template emitter
35
+ * and route them to the appropriate package ID emitter
36
36
  */
37
37
  handleCreateEvent(event) {
38
- const templateId = event.templateId;
39
- if (this.templateEmitters.has(templateId)) {
40
- this.templateEmitters.get(templateId).emit("create", event);
38
+ const packageId = event.templateId;
39
+ if (this.packageIdEmitters.has(packageId)) {
40
+ this.packageIdEmitters.get(packageId).emit("create", event);
41
41
  }
42
42
  else {
43
- logger.warn(`Received create event for unknown template ${templateId}`);
43
+ logger.warn(`Received create event for unknown template ${packageId}`);
44
44
  }
45
45
  }
46
46
  /**
47
47
  * Handle archive events from the underlying stream
48
- * and route them to the appropriate template emitter
48
+ * and route them to the appropriate package ID emitter
49
49
  */
50
50
  handleArchiveEvent(event) {
51
- const templateId = event.templateId;
52
- if (this.templateEmitters.has(templateId)) {
53
- this.templateEmitters.get(templateId).emit("archive", event);
51
+ const packageId = event.templateId;
52
+ if (this.packageIdEmitters.has(packageId)) {
53
+ this.packageIdEmitters.get(packageId).emit("archive", event);
54
54
  }
55
55
  else {
56
- logger.warn(`Received archive event for unknown template ${templateId}`);
56
+ logger.warn(`Received archive event for unknown template ${packageId}`);
57
57
  }
58
58
  }
59
59
  /**
@@ -69,14 +69,14 @@ export class MultiStreamAdapter {
69
69
  }
70
70
  // MultiStream interface implementation
71
71
  onCreate(templateId, listener) {
72
- const emitter = this.getEmitterForTemplate(templateId);
72
+ const emitter = this.getEmitterForPackageId(templateId);
73
73
  emitter.on("create", (event) => {
74
74
  // Type assertion: we know the event has the correct type based on templateId
75
75
  listener(event);
76
76
  });
77
77
  }
78
78
  onArchive(templateId, listener) {
79
- const emitter = this.getEmitterForTemplate(templateId);
79
+ const emitter = this.getEmitterForPackageId(templateId);
80
80
  emitter.on("archive", (event) => {
81
81
  // Type assertion: we know the event has the correct type based on templateId
82
82
  listener(event);
@@ -89,13 +89,13 @@ export class MultiStreamAdapter {
89
89
  this.stream.on("state", listener);
90
90
  }
91
91
  offCreate(templateId, listener) {
92
- if (this.templateEmitters.has(templateId)) {
93
- this.templateEmitters.get(templateId).off("create", listener);
92
+ if (this.packageIdEmitters.has(templateId)) {
93
+ this.packageIdEmitters.get(templateId).off("create", listener);
94
94
  }
95
95
  }
96
96
  offArchive(templateId, listener) {
97
- if (this.templateEmitters.has(templateId)) {
98
- this.templateEmitters.get(templateId).off("archive", listener);
97
+ if (this.packageIdEmitters.has(templateId)) {
98
+ this.packageIdEmitters.get(templateId).off("archive", listener);
99
99
  }
100
100
  }
101
101
  offError(listener) {
@@ -112,12 +112,56 @@ export class MultiStreamAdapter {
112
112
  }
113
113
  close() {
114
114
  this.stream.close();
115
- for (const emitter of this.templateEmitters.values()) {
115
+ for (const emitter of this.packageIdEmitters.values()) {
116
116
  emitter.removeAllListeners();
117
117
  }
118
- this.templateEmitters.clear();
118
+ this.packageIdEmitters.clear();
119
119
  // Clean up the error emitter too
120
120
  this.errorEmitter.removeAllListeners();
121
121
  this.stateEmitter.removeAllListeners();
122
122
  }
123
+ updateToken(newToken) {
124
+ this.stream.updateToken(newToken);
125
+ }
126
+ }
127
+ /**
128
+ * Extends MultiStreamAdapter to add interface-specific event handling
129
+ * Provides both template-specific and interface-specific event handlers with proper typing
130
+ */
131
+ export class InterfaceMultiStreamImpl extends MultiStreamAdapter {
132
+ /**
133
+ * Create a new InterfaceMultiStream adapter
134
+ * @param stream The underlying InterfaceStream instance
135
+ */
136
+ constructor(stream) {
137
+ // Pass the stream to the parent MultiStreamAdapter
138
+ super(stream);
139
+ this.interfaceStream = stream;
140
+ // Set up interface-specific event listener
141
+ this.interfaceStream.on("interfaceView", this.handleInterfaceViewEvent.bind(this));
142
+ }
143
+ /**
144
+ * Handle interfaceView events from the underlying stream
145
+ */
146
+ handleInterfaceViewEvent(event) {
147
+ const interfaceId = event.interfaceId;
148
+ if (this.packageIdEmitters.has(interfaceId)) {
149
+ this.packageIdEmitters.get(interfaceId).emit("interfaceView", event);
150
+ }
151
+ else {
152
+ logger.warn(`Received interfaceView event for unknown interface ${interfaceId}`);
153
+ }
154
+ }
155
+ // InterfaceMultiStreamMethods implementation
156
+ onInterfaceView(interfaceId, listener) {
157
+ const emitter = this.getEmitterForPackageId(interfaceId);
158
+ emitter.on("interfaceView", (event) => {
159
+ listener(event);
160
+ });
161
+ }
162
+ offInterfaceView(interfaceId, listener) {
163
+ if (this.packageIdEmitters.has(interfaceId)) {
164
+ this.packageIdEmitters.get(interfaceId).off("interfaceView", listener);
165
+ }
166
+ }
123
167
  }
@@ -0,0 +1,13 @@
1
+ export interface TokenInfo {
2
+ userId: string;
3
+ expiresAt: number;
4
+ issuedAt: number;
5
+ isExpired: boolean;
6
+ timeUntilExpiry: number;
7
+ }
8
+ /**
9
+ * Log token expiration information
10
+ * @param token The JWT token string
11
+ * @param context Additional context for logging (e.g., "connection", "reconnection")
12
+ */
13
+ export declare function logTokenExpiration(token: string, context?: string): TokenInfo;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * JWT token utilities for Canton ledger authentication
3
+ */
4
+ import { decodeJwt } from "jose";
5
+ import { logger } from "./logger.js";
6
+ /**
7
+ * Decode and analyze a JWT token
8
+ * @param token The JWT token string
9
+ * @returns Token information including expiration details
10
+ */
11
+ function analyzeToken(token) {
12
+ const payload = decodeJwt(token);
13
+ if (!payload.sub) {
14
+ throw new Error("Token payload missing 'sub' field");
15
+ }
16
+ const now = Math.floor(Date.now() / 1000);
17
+ const expiresAt = payload.exp || 0;
18
+ const issuedAt = payload.iat || 0;
19
+ const isExpired = expiresAt > 0 && expiresAt <= now;
20
+ const timeUntilExpiry = expiresAt > 0 ? expiresAt - now : 0;
21
+ return {
22
+ userId: payload.sub,
23
+ expiresAt,
24
+ issuedAt,
25
+ isExpired,
26
+ timeUntilExpiry,
27
+ };
28
+ }
29
+ /**
30
+ * Log token expiration information
31
+ * @param token The JWT token string
32
+ * @param context Additional context for logging (e.g., "connection", "reconnection")
33
+ */
34
+ export function logTokenExpiration(token, context = "token") {
35
+ try {
36
+ const tokenInfo = analyzeToken(token);
37
+ if (tokenInfo.isExpired) {
38
+ logger.warn(`${context}: Token is EXPIRED`, {
39
+ expiresAt: new Date(tokenInfo.expiresAt * 1000).toISOString(),
40
+ expiredSecondsAgo: Math.abs(tokenInfo.timeUntilExpiry),
41
+ userId: tokenInfo.userId,
42
+ });
43
+ }
44
+ else if (tokenInfo.timeUntilExpiry < 300) { // Less than 5 minutes
45
+ logger.warn(`${context}: Token expires soon`, {
46
+ expiresAt: new Date(tokenInfo.expiresAt * 1000).toISOString(),
47
+ secondsUntilExpiry: tokenInfo.timeUntilExpiry,
48
+ userId: tokenInfo.userId,
49
+ });
50
+ }
51
+ else {
52
+ logger.debug(`${context}: Token is valid`, {
53
+ expiresAt: new Date(tokenInfo.expiresAt * 1000).toISOString(),
54
+ secondsUntilExpiry: tokenInfo.timeUntilExpiry,
55
+ userId: tokenInfo.userId,
56
+ });
57
+ }
58
+ return tokenInfo;
59
+ }
60
+ catch (error) {
61
+ logger.error(`${context}: Failed to analyze token`, { error: error instanceof Error ? error.message : String(error) });
62
+ return { userId: "", expiresAt: 0, issuedAt: 0, isExpired: true, timeUntilExpiry: 0 };
63
+ }
64
+ }
@@ -43,6 +43,7 @@ export type Interface<I extends object> = {
43
43
  key?: any;
44
44
  createdEventBlob: string;
45
45
  interfaceView: I;
46
+ interfaceId: PackageIdString;
46
47
  /**
47
48
  * Package version string (e.g., "0.0.6")
48
49
  * Only present if using VersionedRegistry
@@ -67,7 +68,7 @@ export type VersionedLookupResult = {
67
68
  *
68
69
  * This type can be passed into our Ledger so that we can use it instead. We
69
70
  * overload the name of the lookup parameter 'templateId' even though in the
70
- * interface we really mean the interfaceId as in the spec OpenAPI spec.
71
+ * interface we really mean the interfaceId as in the OpenAPI spec.
71
72
  */
72
73
  export type VersionedRegistry = (templateId: string) => VersionedLookupResult | undefined;
73
74
  export type LedgerOffset = "start" | "end" | number;
@@ -78,7 +79,7 @@ export interface StreamCloseEvent {
78
79
  }
79
80
  export type StreamState = "start" | "init" | "live" | "stop";
80
81
  /**
81
- * The return interface of streamQuery and streamQueries.
82
+ * The return interface of streamTemplate - for template-specific streaming with known types.
82
83
  */
83
84
  export interface Stream<T extends object, K> {
84
85
  on(type: "create", listener: (event: CreateEvent<T, K>) => void): void;
@@ -92,7 +93,24 @@ export interface Stream<T extends object, K> {
92
93
  start(): void;
93
94
  state(): StreamState;
94
95
  close(): void;
96
+ updateToken(newToken: string): void;
95
97
  }
98
+ /**
99
+ * Additional interface-specific methods for interface streaming.
100
+ */
101
+ export interface InterfaceStreamMethods<I extends object> {
102
+ on(type: "interfaceView", listener: (event: Interface<I>) => void): void;
103
+ off(type: "interfaceView", listener: (event: Interface<I>) => void): void;
104
+ }
105
+ /**
106
+ * The return interface of streamInterface - combines Stream with interface-specific events.
107
+ *
108
+ * Most of the time, if you are subscribing to an interface, the underlying
109
+ * template is operationally not interesting to you (otherwise, you would be
110
+ * subscribing to the template directly). Consequently we will emit the
111
+ * template related events but not make an effort to decode them.
112
+ */
113
+ export type InterfaceStream<I extends object> = Stream<object, unknown> & InterfaceStreamMethods<I>;
96
114
  /**
97
115
  * Template mapping type for MultiStream:
98
116
  * template IDs to their corresponding contract and key types
@@ -101,6 +119,30 @@ export type TemplateMapping = Record<string, {
101
119
  contractType: object;
102
120
  keyType: unknown;
103
121
  }>;
122
+ /**
123
+ * Interface mapping type for InterfaceMultiStream:
124
+ * interface IDs to their corresponding contract types
125
+ */
126
+ export type InterfaceMapping = Record<string, {
127
+ contractType: object;
128
+ }>;
129
+ /**
130
+ * Interface-specific methods for MultiStream
131
+ */
132
+ export interface InterfaceMultiStreamMethods<IM extends InterfaceMapping> {
133
+ /**
134
+ * Register a listener for interfaceView events for a specific interface
135
+ * @param interfaceId The interface ID to listen for
136
+ * @param listener The callback function that will receive properly typed interface events
137
+ */
138
+ onInterfaceView<IID extends keyof IM>(interfaceId: IID, listener: (event: Interface<IM[IID]["contractType"]>) => void): void;
139
+ /**
140
+ * Remove an interfaceView event listener for a specific interface
141
+ * @param interfaceId The interface ID to stop listening for
142
+ * @param listener The callback function to remove
143
+ */
144
+ offInterfaceView<IID extends keyof IM>(interfaceId: IID, listener: (event: Interface<IM[IID]["contractType"]>) => void): void;
145
+ }
104
146
  /**
105
147
  * Provides template-specific event handlers.
106
148
  */
@@ -144,7 +186,24 @@ export interface MultiStream<TM extends TemplateMapping> {
144
186
  start(): void;
145
187
  state(): StreamState;
146
188
  close(): void;
189
+ updateToken(newToken: string): void;
147
190
  }
191
+ /**
192
+ * Derives a TemplateMapping from an InterfaceMapping
193
+ * Templates underlying interfaces have generic object contractType and unknown keyType
194
+ */
195
+ type DerivedTemplateMapping<IM extends InterfaceMapping> = {
196
+ [K in keyof IM]: {
197
+ contractType: object;
198
+ keyType: unknown;
199
+ };
200
+ };
201
+ /**
202
+ * Extended MultiStream interface that includes interface-specific event handlers
203
+ * Similar to how InterfaceStream extends Stream
204
+ * Derives the template mapping from the interface mapping since interfaces are implemented for templates
205
+ */
206
+ export type InterfaceMultiStream<IM extends InterfaceMapping> = MultiStream<DerivedTemplateMapping<IM>> & InterfaceMultiStreamMethods<IM>;
148
207
  export interface Query<T = unknown> {
149
208
  [key: string]: unknown;
150
209
  }
@@ -199,3 +258,4 @@ export interface User {
199
258
  primaryParty?: Party;
200
259
  rights: UserRight[];
201
260
  }
261
+ export {};
@@ -47,6 +47,15 @@ export declare class WebSocketClient {
47
47
  private wsBaseUrl;
48
48
  private validator?;
49
49
  constructor(config: StreamConfig);
50
+ /**
51
+ * Get the current token (for logging and validation purposes)
52
+ */
53
+ getToken(): string;
54
+ /**
55
+ * Update the token used for authentication
56
+ * Note: This only updates the stored token. Active connections need to be recreated.
57
+ */
58
+ setToken(newToken: string): void;
50
59
  /**
51
60
  * Generic streaming method with full type safety
52
61
  * This method enforces that request/response types match the endpoint
@@ -1,5 +1,6 @@
1
1
  import { SchemaValidator } from "./validation.js";
2
2
  import { logger } from "./logger.js";
3
+ import { logTokenExpiration } from "./token.js";
3
4
  import WebSocket from "isomorphic-ws";
4
5
  // Constant mapping from endpoints to their JSON Schema validation paths
5
6
  const SCHEMA_MAP = {
@@ -54,6 +55,19 @@ export class WebSocketClient {
54
55
  ? new SchemaValidator("asyncapi", config.validation)
55
56
  : undefined;
56
57
  }
58
+ /**
59
+ * Get the current token (for logging and validation purposes)
60
+ */
61
+ getToken() {
62
+ return this.token;
63
+ }
64
+ /**
65
+ * Update the token used for authentication
66
+ * Note: This only updates the stored token. Active connections need to be recreated.
67
+ */
68
+ setToken(newToken) {
69
+ this.token = newToken;
70
+ }
57
71
  /**
58
72
  * Generic streaming method with full type safety
59
73
  * This method enforces that request/response types match the endpoint
@@ -99,6 +113,8 @@ export class WebSocketClient {
99
113
  const url = endpoint.startsWith("/")
100
114
  ? `${baseUrl}${endpoint.slice(1)}`
101
115
  : `${baseUrl}/${endpoint}`;
116
+ // Log token expiration info before connecting
117
+ logTokenExpiration(this.token, `WebSocket connection to ${endpoint}`);
102
118
  // WebSocket authentication via Sec-WebSocket-Protocol header
103
119
  const protocols = [`jwt.token.${this.token}`, "daml.ws.auth"];
104
120
  logger.debug(`Connecting websocket to ${url} with protocols: ${protocols}`);