Skip to content

Commit

Permalink
Merge branch 'main' into build/fix-peculiar/webcrypto-dev-deps
Browse files Browse the repository at this point in the history
  • Loading branch information
krpeacock authored Dec 11, 2024
2 parents 8fda9e0 + 54fe070 commit 1eed0a8
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 37 deletions.
3 changes: 2 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

### Changed

- chore: Removes warning that users found unhelpful, when a message originates from other sources than the identity provider in `AuthClient` during authentication.
- chore: Removes warning that users found unhelpful, when a message originates from other sources than the identity provider in `AuthClient` during authentication.
- fix: Make pollForResponse typesafe to avoid exceptions from unknown requests

## [2.1.3] - 2024-10-23

Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe('makeActor', () => {
},
} as UnSigned<CallRequest>;

const expectedCallRequestId = await requestIdOf(expectedCallRequest.content);
const expectedCallRequestId = requestIdOf(expectedCallRequest.content);

const httpAgent = new HttpAgent({ fetch: mockFetch });

Expand Down
9 changes: 2 additions & 7 deletions packages/agent/src/agent/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,7 @@ export interface Agent {
* `readState` uses this internally.
* Useful to avoid signing the same request multiple times.
*/
createReadStateRequest?(
options: ReadStateOptions,
identity?: Identity,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any>;
createReadStateRequest?(options: ReadStateOptions, identity?: Identity): Promise<unknown>;

/**
* Send a read state query to the replica. This includes a list of paths to return,
Expand All @@ -179,8 +175,7 @@ export interface Agent {
effectiveCanisterId: Principal | string,
options: ReadStateOptions,
identity?: Identity,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request?: any,
request?: unknown,
): Promise<ReadStateResponse>;

call(canisterId: Principal | string, fields: CallOptions): Promise<SubmitResponse>;
Expand Down
12 changes: 6 additions & 6 deletions packages/agent/src/agent/http/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ test('call', async () => {
ingress_expiry: new Expiry(300000),
};

const mockPartialsRequestId = await requestIdOf(mockPartialRequest);
const mockPartialsRequestId = requestIdOf(mockPartialRequest);

const expectedRequest = {
content: mockPartialRequest,
};

const expectedRequestId = await requestIdOf(expectedRequest.content);
const expectedRequestId = requestIdOf(expectedRequest.content);
expect(expectedRequestId).toEqual(mockPartialsRequestId);

const { calls } = mockFetch.mock;
Expand Down Expand Up @@ -150,7 +150,7 @@ test('queries with the same content should have the same signature', async () =>
const methodName = 'greet';
const arg = new Uint8Array([]);

const requestId = await requestIdOf({
const requestId = requestIdOf({
request_type: SubmitRequestType.Call,
nonce,
canister_id: Principal.fromText(canisterIdent).toString(),
Expand Down Expand Up @@ -213,7 +213,7 @@ test('readState should not call transformers if request is passed', async () =>
const methodName = 'greet';
const arg = new Uint8Array([]);

const requestId = await requestIdOf({
const requestId = requestIdOf({
request_type: SubmitRequestType.Call,
nonce,
canister_id: Principal.fromText(canisterIdent).toString(),
Expand Down Expand Up @@ -314,13 +314,13 @@ test('use anonymous principal if unspecified', async () => {
ingress_expiry: new Expiry(300000),
};

const mockPartialsRequestId = await requestIdOf(mockPartialRequest);
const mockPartialsRequestId = requestIdOf(mockPartialRequest);

const expectedRequest: Envelope<CallRequest> = {
content: mockPartialRequest,
};

const expectedRequestId = await requestIdOf(expectedRequest.content);
const expectedRequestId = requestIdOf(expectedRequest.content);
expect(expectedRequestId).toEqual(mockPartialsRequestId);

const { calls } = mockFetch.mock;
Expand Down
16 changes: 7 additions & 9 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export class HttpAgent implements Agent {
): Promise<SubmitResponse> {
// TODO - restore this value
const callSync = options.callSync ?? true;
const id = await(identity !== undefined ? await identity : await this.#identity);
const id = await (identity !== undefined ? await identity : await this.#identity);
if (!id) {
throw new IdentityInvalidError(
"This identity has expired due this application's security policy. Please refresh your authentication.",
Expand Down Expand Up @@ -468,8 +468,7 @@ export class HttpAgent implements Agent {
ingress_expiry,
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let transformedRequest: any = (await this._transform({
let transformedRequest = (await this._transform({
request: {
body: null,
method: 'POST',
Expand All @@ -493,7 +492,7 @@ export class HttpAgent implements Agent {
}

// Apply transform for identity.
transformedRequest = await id.transformRequest(transformedRequest);
transformedRequest = (await id.transformRequest(transformedRequest)) as HttpAgentSubmitRequest;

const body = cbor.encode(transformedRequest.body);
const backoff = this.#backoffStrategy();
Expand Down Expand Up @@ -528,9 +527,9 @@ export class HttpAgent implements Agent {
backoff,
tries: 0,
});
const requestId = requestIdOf(submit);

const [response, requestId] = await Promise.all([request, requestIdOf(submit)]);

const response = await request;
const responseBuffer = await response.arrayBuffer();
const responseBody = (
response.status === 200 && responseBuffer.byteLength > 0
Expand Down Expand Up @@ -780,7 +779,7 @@ export class HttpAgent implements Agent {
this.log.print(`ecid ${ecid.toString()}`);
this.log.print(`canisterId ${canisterId.toString()}`);
const makeQuery = async () => {
const id = await(identity !== undefined ? identity : this.#identity);
const id = await (identity !== undefined ? identity : this.#identity);
if (!id) {
throw new IdentityInvalidError(
"This identity has expired due this application's security policy. Please refresh your authentication.",
Expand All @@ -799,7 +798,7 @@ export class HttpAgent implements Agent {
ingress_expiry: new Expiry(this.#maxIngressExpiryInMinutes * MINUTE_TO_MSECS),
};

const requestId = await requestIdOf(request);
const requestId = requestIdOf(request);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let transformedRequest: HttpAgentRequest = await this._transform({
Expand Down Expand Up @@ -1176,4 +1175,3 @@ export class HttpAgent implements Agent {
return p;
}
}

2 changes: 1 addition & 1 deletion packages/agent/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export abstract class SignIdentity implements Identity {
*/
public async transformRequest(request: HttpAgentRequest): Promise<unknown> {
const { body, ...fields } = request;
const requestId = await requestIdOf(body);
const requestId = requestIdOf(body);
return {
...fields,
body: {
Expand Down
58 changes: 53 additions & 5 deletions packages/agent/src/polling/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toHex } from '../utils/buffer';
export * as strategy from './strategy';
import { defaultStrategy } from './strategy';
import { DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS } from '../constants';
import { ReadRequestType, ReadStateRequest } from '../agent/http/types';
export { defaultStrategy } from './strategy';
export type PollStrategy = (
canisterId: Principal,
Expand All @@ -15,23 +16,68 @@ export type PollStrategy = (
) => Promise<void>;
export type PollStrategyFactory = () => PollStrategy;

interface SignedReadStateRequestWithExpiry {
body: {
content: Pick<ReadStateRequest, 'request_type' | 'ingress_expiry'>;
};
}

/**
* Check if an object has a property
* @param value the object that might have the property
* @param property the key of property we're looking for
*/
function hasProperty<O extends object, P extends string>(
value: O,
property: P,
): value is O & Record<P, unknown> {
return Object.prototype.hasOwnProperty.call(value, property);
}

/**
* Check if value is a signed read state request with expiry
* @param value to check
*/
function isSignedReadStateRequestWithExpiry(
value: unknown,
): value is SignedReadStateRequestWithExpiry {
return (
value !== null &&
typeof value === 'object' &&
hasProperty(value, 'body') &&
value.body !== null &&
typeof value.body === 'object' &&
hasProperty(value.body, 'content') &&
value.body.content !== null &&
typeof value.body.content === 'object' &&
hasProperty(value.body.content, 'request_type') &&
value.body.content.request_type === ReadRequestType.ReadState &&
hasProperty(value.body.content, 'ingress_expiry') &&
typeof value.body.content.ingress_expiry === 'object' &&
value.body.content.ingress_expiry !== null &&
hasProperty(value.body.content.ingress_expiry, 'toCBOR') &&
typeof value.body.content.ingress_expiry.toCBOR === 'function' &&
hasProperty(value.body.content.ingress_expiry, 'toHash') &&
typeof value.body.content.ingress_expiry.toHash === 'function'
);
}

/**
* Polls the IC to check the status of the given request then
* returns the response bytes once the request has been processed.
* @param agent The agent to use to poll read_state.
* @param canisterId The effective canister ID.
* @param requestId The Request ID to poll status for.
* @param strategy A polling strategy.
* @param request Request for the readState call.
* @param request Request for the repeated readState call.
* @param blsVerify - optional replacement function that verifies the BLS signature of a certificate.
*/
export async function pollForResponse(
agent: Agent,
canisterId: Principal,
requestId: RequestId,
strategy: PollStrategy = defaultStrategy(),
// eslint-disable-next-line
request?: any,
request?: unknown,
blsVerify?: CreateCertificateOptions['blsVerify'],
): Promise<{
certificate: Certificate;
Expand All @@ -40,8 +86,10 @@ export async function pollForResponse(
const path = [new TextEncoder().encode('request_status'), requestId];
const currentRequest = request ?? (await agent.createReadStateRequest?.({ paths: [path] }));

// Use a fresh expiry for the readState call.
currentRequest.body.content.ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);
// Use a fresh expiry for the repeated readState call
if (request && isSignedReadStateRequestWithExpiry(currentRequest)) {
currentRequest.body.content.ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);
}

const state = await agent.readState(canisterId, { paths: [path] }, undefined, currentRequest);
if (agent.rootKey == null) throw new Error('Agent root key not initialized before polling');
Expand Down
8 changes: 4 additions & 4 deletions packages/agent/src/request_id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test('requestIdOf', async () => {
arg: new Uint8Array([68, 73, 68, 76, 0, 253, 42]),
};

const requestId = await requestIdOf(request);
const requestId = requestIdOf(request);

expect(toHex(requestId)).toEqual(
'8781291c347db32a9d8c10eb62b710fce5a93be676474c42babc74c51858f94b',
Expand All @@ -90,7 +90,7 @@ test.skip('requestIdOf for sender_delegation signature', async () => {
new Uint8Array([0, 0, 0, 0, 0, 32, 0, 43, 1, 1]),
],
};
const delegation1ActualHashBytes = await requestIdOf(delegation1);
const delegation1ActualHashBytes = requestIdOf(delegation1);
expect(toHex(delegation1ActualHashBytes)).toEqual(expectedHashBytes);

// Note: this uses `bigint` and blobs, which the rest of this lib uses too.
Expand All @@ -101,15 +101,15 @@ test.skip('requestIdOf for sender_delegation signature', async () => {
targets: delegation1.targets.map(t => t),
expiration: BigInt(delegation1.expiration.toString()),
};
const delegation2ActualHashBytes = await requestIdOf(delegation2);
const delegation2ActualHashBytes = requestIdOf(delegation2);
expect(toHex(delegation2ActualHashBytes)).toEqual(toHex(delegation1ActualHashBytes));

// This one uses Principals as targets
const delegation3 = {
...delegation1,
targets: delegation1.targets.map(t => Principal.fromText(t.toString())),
};
const delegation3ActualHashBytes = await requestIdOf(delegation3);
const delegation3ActualHashBytes = requestIdOf(delegation3);
expect(toHex(delegation3ActualHashBytes)).toEqual(toHex(delegation1ActualHashBytes));
});

Expand Down
3 changes: 1 addition & 2 deletions packages/agent/src/request_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ const hashString = (value: string): ArrayBuffer => {
* https://sdk.dfinity.org/docs/interface-spec/index.html#hash-of-map
* @param request - ic-ref request to hash into RequestId
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function requestIdOf(request: Record<string, any>): RequestId {
export function requestIdOf(request: Record<string, unknown>): RequestId {
return hashOfMap(request) as RequestId;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/identity/src/identity/delegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async function _createSingleDelegation(
// a user gesture if you await an async call thats not fetch, xhr, or setTimeout.
const challenge = new Uint8Array([
...domainSeparator,
...new Uint8Array(requestIdOf(delegation)),
...new Uint8Array(requestIdOf({ ...delegation })),
]);
const signature = await from.sign(challenge);

Expand Down

0 comments on commit 1eed0a8

Please sign in to comment.