@jsm-mit/sultana-assets-rust-package 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -34,7 +34,8 @@ One class per part of the canister:
34
34
  | `AssetsRustUploadsActor` | chunked session, single-shot upload, `uploadBytesAsyncUnsafe` | any signed-in caller |
35
35
  | `AssetsRustSalonActor` | `upload_image`, `delete_salon_asset`, transcoding and render reservations | the salon's owner — core decides |
36
36
  | `AssetsRustWorkerActor` | system upload of a transcoded result under a reserved id | listed transcoding workers |
37
- | `AssetsRustAssetsActor` | `get_asset_info`, `list_assets`, `http_request`, `assetUrl` | anybody |
37
+ | `AssetsRustAssetsActor` | `get_asset_info`, `list_assets`, `http_request`, `assetUrl` | anybody; `list_assets` only the canister's controllers and the core seat |
38
+ | `AssetsRustAvatarActor` | `upload_my_avatar`, `delete_my_avatar` (own avatar only, profile required — core `isUser` —, no quota; 64 KB / 512 px, re-encoded as JPEG), `get_avatar`, `/avatar/<principal>`, `avatarUrl` | writes: the caller for themselves; reads: anybody |
38
39
 
39
40
  Every method is `…AsyncUnsafe` and throws one of three errors, like the other wrapper
40
41
  packages:
@@ -56,7 +57,8 @@ types are exported too, and `./declarations/*` gives the generated files.
56
57
  `src/` is browser-safe — the same package serves the biz panel, the playground and the
57
58
  transcoding worker.
58
59
 
59
- Scripts: `build`, `typecheck`, `sync-declarations`, `test-suite`, `test-smoke`, `test-upgrade`, `redeploy`, `sandbox`
60
+ Scripts: `build`, `typecheck`, `sync-declarations`, `test-suite`, `test-smoke`, `test-upgrade`,
61
+ `test-suite-local`, `test-smoke-local`, `test-upgrade-local`, `redeploy`, `sandbox`
60
62
  (an interactive console: `npm run sandbox -- <canister-id>`, caller from `IDENTITY_PEM` in
61
63
  `.env.sandbox` or anonymous), `stamp-canister`, `publish-public`. See `CLAUDE.md` for who runs what.
62
64
 
@@ -72,6 +74,12 @@ read per domain (worker list, a few-byte single-shot upload read back over HTTP,
72
74
  result, a salon upload refused by the stand-in core), one replication wait, ≈ 30 s.
73
75
  `publish-public` runs it too.
74
76
 
77
+ The `-local` variants (`npm run test-suite-local`, `test-smoke-local`, `test-upgrade-local`) run
78
+ the same entry points on the LOCAL replica — no cycles, no lease. `scripts/local-canister.sh`
79
+ builds `../sultana-assets-rust` with cargo and reinstalls it into a canister of the dfx project
80
+ in `local/` (its id stays under `local/.dfx`); the upgrade test upgrades that canister. The
81
+ local replica must run (`dfx start --background`); the scripts never start or stop it.
82
+
75
83
  What the suite covers: the core seat and the worker list; chunked and single-shot uploads
76
84
  with every owner and size check; reads, HTTP 200/206/404, certification header and
77
85
  streaming; the worker's system upload with a poster; `delete_asset` taking the thumb along;
@@ -84,7 +92,8 @@ the suite never touches it.
84
92
 
85
93
  Bump the version by hand, commit, push, then `npm run publish-public`. It refuses unless this
86
94
  repo and `../sultana-assets-rust` are on a clean, pushed `master` and the version is new on
87
- npm; it reinstalls the canister's master into `short-living` (lease 2), checks that its
95
+ npm; it reinstalls the canister's master into the canister on the LOCAL replica
96
+ (`scripts/local-canister.sh`; the replica must run), checks that its
88
97
  `candid:service` equals `declarations/sultana-assets-rust/sultana-assets-rust.did` as dfx
89
98
  rewrites it (the hand-written .did is normalized by a local `dfx build --check` first), runs
90
99
  the smoke test (`tests/test-smoke.ts`) on that same deploy, stamps
@@ -75,11 +75,13 @@ type ReserveRenderArgs = record {
75
75
  service : () -> {
76
76
  abort_upload : (text) -> (bool);
77
77
  delete_asset : (text) -> (bool);
78
+ delete_my_avatar : () -> (bool);
78
79
  delete_salon_asset : (DeleteSalonAssetArgs) -> (bool);
79
80
  fail_system_upload : (FailSystemUploadArgs) -> (bool);
80
81
  finish_system_upload : (FinishSystemUploadArgs) -> (text);
81
82
  finish_upload : (text) -> (text);
82
83
  get_asset_info : (text) -> (opt AssetInfo) query;
84
+ get_avatar : (principal) -> (opt text) query;
83
85
  get_coordinator : () -> (opt principal) query;
84
86
  get_core_canister : () -> (opt principal) query;
85
87
  get_workers : () -> (vec WorkerEntry) query;
@@ -97,4 +99,5 @@ service : () -> {
97
99
  upload_asset : (UploadArgs) -> (text);
98
100
  upload_chunk : (text, nat32, blob) -> ();
99
101
  upload_image : (UploadImageArgs) -> (text);
102
+ upload_my_avatar : (blob) -> (text);
100
103
  }
@@ -95,11 +95,13 @@ export interface WorkerEntry { 'url' : string, 'principal' : Principal }
95
95
  export interface _SERVICE {
96
96
  'abort_upload' : ActorMethod<[string], boolean>,
97
97
  'delete_asset' : ActorMethod<[string], boolean>,
98
+ 'delete_my_avatar' : ActorMethod<[], boolean>,
98
99
  'delete_salon_asset' : ActorMethod<[DeleteSalonAssetArgs], boolean>,
99
100
  'fail_system_upload' : ActorMethod<[FailSystemUploadArgs], boolean>,
100
101
  'finish_system_upload' : ActorMethod<[FinishSystemUploadArgs], string>,
101
102
  'finish_upload' : ActorMethod<[string], string>,
102
103
  'get_asset_info' : ActorMethod<[string], [] | [AssetInfo]>,
104
+ 'get_avatar' : ActorMethod<[Principal], [] | [string]>,
103
105
  'get_coordinator' : ActorMethod<[], [] | [Principal]>,
104
106
  'get_core_canister' : ActorMethod<[], [] | [Principal]>,
105
107
  'get_workers' : ActorMethod<[], Array<WorkerEntry>>,
@@ -126,6 +128,7 @@ export interface _SERVICE {
126
128
  undefined
127
129
  >,
128
130
  'upload_image' : ActorMethod<[UploadImageArgs], string>,
131
+ 'upload_my_avatar' : ActorMethod<[Uint8Array | number[]], string>,
129
132
  }
130
133
  export declare const idlFactory: IDL.InterfaceFactory;
131
134
  export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];
@@ -106,11 +106,13 @@ export const idlFactory = ({ IDL }) => {
106
106
  return IDL.Service({
107
107
  'abort_upload' : IDL.Func([IDL.Text], [IDL.Bool], []),
108
108
  'delete_asset' : IDL.Func([IDL.Text], [IDL.Bool], []),
109
+ 'delete_my_avatar' : IDL.Func([], [IDL.Bool], []),
109
110
  'delete_salon_asset' : IDL.Func([DeleteSalonAssetArgs], [IDL.Bool], []),
110
111
  'fail_system_upload' : IDL.Func([FailSystemUploadArgs], [IDL.Bool], []),
111
112
  'finish_system_upload' : IDL.Func([FinishSystemUploadArgs], [IDL.Text], []),
112
113
  'finish_upload' : IDL.Func([IDL.Text], [IDL.Text], []),
113
114
  'get_asset_info' : IDL.Func([IDL.Text], [IDL.Opt(AssetInfo)], ['query']),
115
+ 'get_avatar' : IDL.Func([IDL.Principal], [IDL.Opt(IDL.Text)], ['query']),
114
116
  'get_coordinator' : IDL.Func([], [IDL.Opt(IDL.Principal)], ['query']),
115
117
  'get_core_canister' : IDL.Func([], [IDL.Opt(IDL.Principal)], ['query']),
116
118
  'get_workers' : IDL.Func([], [IDL.Vec(WorkerEntry)], ['query']),
@@ -140,6 +142,7 @@ export const idlFactory = ({ IDL }) => {
140
142
  'upload_asset' : IDL.Func([UploadArgs], [IDL.Text], []),
141
143
  'upload_chunk' : IDL.Func([IDL.Text, IDL.Nat32, IDL.Vec(IDL.Nat8)], [], []),
142
144
  'upload_image' : IDL.Func([UploadImageArgs], [IDL.Text], []),
145
+ 'upload_my_avatar' : IDL.Func([IDL.Vec(IDL.Nat8)], [IDL.Text], []),
143
146
  });
144
147
  };
145
148
  export const init = ({ IDL }) => { return []; };
@@ -25,10 +25,13 @@ export declare class ActorBase {
25
25
  protected host: string;
26
26
  protected actor: ActorSubclass<_SERVICE>;
27
27
  protected agent: HttpAgent;
28
+ private ready;
28
29
  /**
29
30
  * @param canisterId - The sultana-assets-rust canister to talk to.
30
31
  * @param identity - The caller; left out = anonymous (enough for the reads).
31
- * @param host - The IC API host; default https://icp0.io.
32
+ * @param host - The IC API host; default https://icp0.io. A local replica
33
+ * (`http://127.0.0.1:4943`) works too: for a local host — and only then — the agent
34
+ * fetches the replica's root key before the first call.
32
35
  */
33
36
  constructor(canisterId: string, identity?: Identity | undefined, host?: string);
34
37
  private initAgentAndActor;
@@ -1 +1 @@
1
- {"version":3,"file":"actor-base.d.ts","sourceRoot":"","sources":["../src/actor-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAS,MAAM,qBAAqB,CAAC;AAE1F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gEAAgE,CAAC;AAG/F,sFAAsF;AACtF,MAAM,WAAW,SAAS;IACtB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;CACrB;AAKD;;;;;;;;;GASG;AACH,qBAAa,SAAS;IASN,SAAS,CAAC,UAAU,EAAE,MAAM;IAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,QAAQ;IAAE,SAAS,CAAC,IAAI,EAAE,MAAM;IAR/F,SAAS,CAAC,KAAK,EAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1C,SAAS,CAAC,KAAK,EAAG,SAAS,CAAC;IAE5B;;;;OAIG;gBACmB,UAAU,EAAE,MAAM,EAAY,QAAQ,CAAC,EAAE,QAAQ,YAAA,EAAY,IAAI,GAAE,MAAqB;IAI9G,OAAO,CAAC,iBAAiB;IAWzB;;;;OAIG;IACI,iBAAiB,IAAI,IAAI;IAIhC,iFAAiF;cACjE,0BAA0B,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBpF,SAAS,CAAC,qBAAqB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO;IASpD,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI;CAU1D"}
1
+ {"version":3,"file":"actor-base.d.ts","sourceRoot":"","sources":["../src/actor-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAS,MAAM,qBAAqB,CAAC;AAE1F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gEAAgE,CAAC;AAG/F,sFAAsF;AACtF,MAAM,WAAW,SAAS;IACtB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;;;;GASG;AACH,qBAAa,SAAS;IAYN,SAAS,CAAC,UAAU,EAAE,MAAM;IAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,QAAQ;IAAE,SAAS,CAAC,IAAI,EAAE,MAAM;IAX/F,SAAS,CAAC,KAAK,EAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1C,SAAS,CAAC,KAAK,EAAG,SAAS,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAoC;IAEjD;;;;;;OAMG;gBACmB,UAAU,EAAE,MAAM,EAAY,QAAQ,CAAC,EAAE,QAAQ,YAAA,EAAY,IAAI,GAAE,MAAqB;IAI9G,OAAO,CAAC,iBAAiB;IAYzB;;;;OAIG;IACI,iBAAiB,IAAI,IAAI;IAIhC,iFAAiF;cACjE,0BAA0B,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuBpF,SAAS,CAAC,qBAAqB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO;IASpD,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI;CAU1D"}
@@ -1,6 +1,9 @@
1
1
  import { HttpAgent, Actor } from "@icp-sdk/core/agent";
2
2
  import { idlFactory } from "../declarations/sultana-assets-rust/sultana-assets-rust.did.js";
3
3
  import { DEFAULT_HOST } from "./constants.js";
4
+ function isLocalHost(host) {
5
+ return /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?\/?$/.test(host);
6
+ }
4
7
  // IC error code of an explicit trap (ic0.trap — every ic_cdk::trap and every Rust panic).
5
8
  const TRAP_ERROR_CODE = "IC0503";
6
9
  /**
@@ -19,10 +22,13 @@ export class ActorBase {
19
22
  host;
20
23
  actor;
21
24
  agent;
25
+ ready = Promise.resolve();
22
26
  /**
23
27
  * @param canisterId - The sultana-assets-rust canister to talk to.
24
28
  * @param identity - The caller; left out = anonymous (enough for the reads).
25
- * @param host - The IC API host; default https://icp0.io.
29
+ * @param host - The IC API host; default https://icp0.io. A local replica
30
+ * (`http://127.0.0.1:4943`) works too: for a local host — and only then — the agent
31
+ * fetches the replica's root key before the first call.
26
32
  */
27
33
  constructor(canisterId, identity, host = DEFAULT_HOST) {
28
34
  this.canisterId = canisterId;
@@ -35,6 +41,7 @@ export class ActorBase {
35
41
  host: this.host,
36
42
  identity: this.identity,
37
43
  });
44
+ this.ready = isLocalHost(this.host) ? this.agent.fetchRootKey().then(() => undefined) : Promise.resolve();
38
45
  this.actor = Actor.createActor(idlFactory, {
39
46
  agent: this.agent,
40
47
  canisterId: this.canisterId,
@@ -50,6 +57,7 @@ export class ActorBase {
50
57
  }
51
58
  /** Runs one canister call and translates its failure (see the class comment). */
52
59
  async executeFunctionAsyncUnsafe(fnAsync) {
60
+ await this.ready;
53
61
  try {
54
62
  return await fnAsync();
55
63
  }
@@ -1,6 +1,7 @@
1
1
  import { ActorBase } from "../actor-base.js";
2
2
  import type { AssetInfoView, HttpGetInput, HttpResponseView } from "../interfaces.js";
3
- /** Reads: what is stored and how it is served. All queries — anonymous callers are fine. */
3
+ /** Reads: what is stored and how it is served. All queries — anonymous callers are fine,
4
+ * except `listAssetsAsyncUnsafe`: the canister's controllers and the core seat only. */
4
5
  export declare class AssetsRustAssetsActor extends ActorBase {
5
6
  /**
6
7
  * @param assetId - The asset.
@@ -13,7 +14,8 @@ export declare class AssetsRustAssetsActor extends ActorBase {
13
14
  getAssetInfoAsyncUnsafe(assetId: string): Promise<AssetInfoView | null>;
14
15
  /**
15
16
  * Every asset physically stored — the raw storage view, independent of any salon library:
16
- * thumbnails, posters and orphans included.
17
+ * thumbnails, posters and orphans included. The canister's controllers and the core seat
18
+ * only; anybody else gets a CanisterError ("list_assets refused: controllers only").
17
19
  * @returns A Promise that resolves to every stored asset.
18
20
  * @throws CanisterError - The canister trapped.
19
21
  * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
@@ -1 +1 @@
1
- {"version":3,"file":"assets-actor.d.ts","sourceRoot":"","sources":["../../src/actors/assets-actor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,4FAA4F;AAC5F,qBAAa,qBAAsB,SAAQ,SAAS;IAChD;;;;;;;OAOG;IACU,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAKpF;;;;;;;OAOG;IACU,qBAAqB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAK9D;;;;;;;;;;OAUG;IACU,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAYzG,0EAA0E;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAG3C"}
1
+ {"version":3,"file":"assets-actor.d.ts","sourceRoot":"","sources":["../../src/actors/assets-actor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGtF;wFACwF;AACxF,qBAAa,qBAAsB,SAAQ,SAAS;IAChD;;;;;;;OAOG;IACU,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAKpF;;;;;;;;OAQG;IACU,qBAAqB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAK9D;;;;;;;;;;OAUG;IACU,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAYzG,0EAA0E;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAG3C"}
@@ -1,7 +1,8 @@
1
1
  import { ActorBase } from "../actor-base.js";
2
2
  import { assetUrl } from "../constants.js";
3
3
  import { mapAssetInfoView, mapHttpResponseView } from "../mappers.js";
4
- /** Reads: what is stored and how it is served. All queries — anonymous callers are fine. */
4
+ /** Reads: what is stored and how it is served. All queries — anonymous callers are fine,
5
+ * except `listAssetsAsyncUnsafe`: the canister's controllers and the core seat only. */
5
6
  export class AssetsRustAssetsActor extends ActorBase {
6
7
  /**
7
8
  * @param assetId - The asset.
@@ -17,7 +18,8 @@ export class AssetsRustAssetsActor extends ActorBase {
17
18
  }
18
19
  /**
19
20
  * Every asset physically stored — the raw storage view, independent of any salon library:
20
- * thumbnails, posters and orphans included.
21
+ * thumbnails, posters and orphans included. The canister's controllers and the core seat
22
+ * only; anybody else gets a CanisterError ("list_assets refused: controllers only").
21
23
  * @returns A Promise that resolves to every stored asset.
22
24
  * @throws CanisterError - The canister trapped.
23
25
  * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
@@ -0,0 +1,54 @@
1
+ import { ActorBase } from "../actor-base.js";
2
+ import type { HttpGetInput, HttpResponseView } from "../interfaces.js";
3
+ /**
4
+ * The caller's own avatar. Every signed-in principal has at most one and is the only one who
5
+ * can change it — there is no principal argument on the writes. Only a principal with a
6
+ * profile in sultana-core-motoko may upload one (core's isUser); there is no salon and no quota. The canister refuses a file over
7
+ * `ASSETS_RUST_LIMITS.avatarMaxBytes` or over `avatarMaxSide` px and stores it re-encoded as
8
+ * JPEG (EXIF/GPS dropped), so scale it down on the client first.
9
+ */
10
+ export declare class AssetsRustAvatarActor extends ActorBase {
11
+ /**
12
+ * Stores a new avatar for the caller and deletes the previous one.
13
+ * @param data - The image (JPEG, PNG or WebP).
14
+ * @returns A Promise that resolves to the new avatar's asset id.
15
+ * @throws CanisterError - The canister trapped: anonymous caller, "avatar too large: …",
16
+ * "avatar refused: …" (no profile in core, or core unreachable), "avatar rejected:
17
+ * …" (not an image, or a side over the limit).
18
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
19
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
20
+ */
21
+ uploadMyAvatarAsyncUnsafe(data: Uint8Array): Promise<string>;
22
+ /**
23
+ * Deletes the caller's avatar.
24
+ * @returns A Promise that resolves to `true` when an avatar was removed, `false` when the
25
+ * caller had none.
26
+ * @throws CanisterError - The canister trapped: anonymous caller.
27
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
28
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
29
+ */
30
+ deleteMyAvatarAsyncUnsafe(): Promise<boolean>;
31
+ /**
32
+ * @param owner - The principal (text).
33
+ * @returns A Promise that resolves to the asset id of the principal's current avatar, or
34
+ * `null` when they have none.
35
+ * @throws CanisterError - The canister trapped.
36
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
37
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
38
+ */
39
+ getAvatarAsyncUnsafe(owner: string): Promise<string | null>;
40
+ /**
41
+ * GET of `/avatar/<owner>` through the canister's `http_request` query — what the HTTP
42
+ * gateway calls: 200 with the current avatar (5-minute cache), 404 when there is none.
43
+ * @param owner - The principal (text).
44
+ * @param input - Request headers.
45
+ * @returns A Promise that resolves to the response.
46
+ * @throws CanisterError - The canister trapped.
47
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
48
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
49
+ */
50
+ httpGetAvatarAsyncUnsafe(owner: string, input?: HttpGetInput): Promise<HttpResponseView>;
51
+ /** The stable public URL of a principal's avatar on this canister (see {@link avatarUrl}). */
52
+ avatarUrl(owner: string): string;
53
+ }
54
+ //# sourceMappingURL=avatar-actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar-actor.d.ts","sourceRoot":"","sources":["../../src/actors/avatar-actor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGvE;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,SAAS;IAChD;;;;;;;;;OASG;IACU,yBAAyB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAIzE;;;;;;;OAOG;IACU,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC;IAI1D;;;;;;;OAOG;IACU,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKxE;;;;;;;;;OASG;IACU,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAYzG,8FAA8F;IACvF,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAG1C"}
@@ -0,0 +1,72 @@
1
+ import { Principal } from "@icp-sdk/core/principal";
2
+ import { ActorBase } from "../actor-base.js";
3
+ import { avatarUrl } from "../constants.js";
4
+ import { mapHttpResponseView } from "../mappers.js";
5
+ /**
6
+ * The caller's own avatar. Every signed-in principal has at most one and is the only one who
7
+ * can change it — there is no principal argument on the writes. Only a principal with a
8
+ * profile in sultana-core-motoko may upload one (core's isUser); there is no salon and no quota. The canister refuses a file over
9
+ * `ASSETS_RUST_LIMITS.avatarMaxBytes` or over `avatarMaxSide` px and stores it re-encoded as
10
+ * JPEG (EXIF/GPS dropped), so scale it down on the client first.
11
+ */
12
+ export class AssetsRustAvatarActor extends ActorBase {
13
+ /**
14
+ * Stores a new avatar for the caller and deletes the previous one.
15
+ * @param data - The image (JPEG, PNG or WebP).
16
+ * @returns A Promise that resolves to the new avatar's asset id.
17
+ * @throws CanisterError - The canister trapped: anonymous caller, "avatar too large: …",
18
+ * "avatar refused: …" (no profile in core, or core unreachable), "avatar rejected:
19
+ * …" (not an image, or a side over the limit).
20
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
21
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
22
+ */
23
+ async uploadMyAvatarAsyncUnsafe(data) {
24
+ return this.executeFunctionAsyncUnsafe(() => this.actor.upload_my_avatar(data));
25
+ }
26
+ /**
27
+ * Deletes the caller's avatar.
28
+ * @returns A Promise that resolves to `true` when an avatar was removed, `false` when the
29
+ * caller had none.
30
+ * @throws CanisterError - The canister trapped: anonymous caller.
31
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
32
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
33
+ */
34
+ async deleteMyAvatarAsyncUnsafe() {
35
+ return this.executeFunctionAsyncUnsafe(() => this.actor.delete_my_avatar());
36
+ }
37
+ /**
38
+ * @param owner - The principal (text).
39
+ * @returns A Promise that resolves to the asset id of the principal's current avatar, or
40
+ * `null` when they have none.
41
+ * @throws CanisterError - The canister trapped.
42
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
43
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
44
+ */
45
+ async getAvatarAsyncUnsafe(owner) {
46
+ const result = await this.executeFunctionAsyncUnsafe(() => this.actor.get_avatar(Principal.fromText(owner)));
47
+ return result.length === 0 ? null : result[0];
48
+ }
49
+ /**
50
+ * GET of `/avatar/<owner>` through the canister's `http_request` query — what the HTTP
51
+ * gateway calls: 200 with the current avatar (5-minute cache), 404 when there is none.
52
+ * @param owner - The principal (text).
53
+ * @param input - Request headers.
54
+ * @returns A Promise that resolves to the response.
55
+ * @throws CanisterError - The canister trapped.
56
+ * @throws CallRefusedAtInspectionStage - The call was refused at the inspect stage.
57
+ * @throws CriticalCanisterError - Transport failure or an unreachable canister.
58
+ */
59
+ async httpGetAvatarAsyncUnsafe(owner, input = {}) {
60
+ const response = await this.executeFunctionAsyncUnsafe(() => this.actor.http_request({
61
+ method: "GET",
62
+ url: `/avatar/${owner}`,
63
+ headers: Object.entries(input.headers ?? {}),
64
+ body: new Uint8Array(),
65
+ }));
66
+ return mapHttpResponseView(response);
67
+ }
68
+ /** The stable public URL of a principal's avatar on this canister (see {@link avatarUrl}). */
69
+ avatarUrl(owner) {
70
+ return avatarUrl(this.canisterId, owner);
71
+ }
72
+ }
@@ -21,12 +21,22 @@ export declare const ASSETS_RUST_LIMITS: {
21
21
  readonly maxMusicStartSec: 600;
22
22
  readonly minSlideDurationSec: 5;
23
23
  readonly maxSlideDurationSec: 60;
24
+ /** `upload_my_avatar`: the largest file, the largest side in px. Scale the image down
25
+ * on the client first (256 px JPEG fits well). */
26
+ readonly avatarMaxBytes: number;
27
+ readonly avatarMaxSide: 512;
24
28
  };
25
29
  /**
26
30
  * The public URL of a stored asset. Served from the raw host (`<id>.raw.icp0.io`), which
27
31
  * also answers Range requests (206, uncertified) that a `<video>` needs to seek.
28
32
  */
29
33
  export declare function assetUrl(canisterId: string, assetId: string): string;
34
+ /**
35
+ * The stable public URL of a principal's avatar: always their current one, 404 when they have
36
+ * none. Cached for 5 minutes, so a new avatar shows up for others within that time; add a
37
+ * query such as `?v=<asset id>` to see a fresh one at once.
38
+ */
39
+ export declare function avatarUrl(canisterId: string, owner: string): string;
30
40
  /** Splits `bytes` into chunks the canister accepts. An empty input is one empty chunk —
31
41
  * a session needs at least one. */
32
42
  export declare function splitIntoChunks(bytes: Uint8Array, chunkSizeBytes?: number): Uint8Array[];
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB;IAC3B;iFAC6E;;IAE7E,yDAAyD;;IAEzD,wEAAwE;;IAExE,uEAAuE;;IAEvE,6EAA6E;;IAE7E,6FAA6F;;;;;CAKvF,CAAC;AAEX;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;oCACoC;AACpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,cAAc,GAAE,MAA0C,GAAG,UAAU,EAAE,CAY3H"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB;IAC3B;iFAC6E;;IAE7E,yDAAyD;;IAEzD,wEAAwE;;IAExE,uEAAuE;;IAEvE,6EAA6E;;IAE7E,6FAA6F;;;;;IAK7F;uDACmD;;;CAG7C,CAAC;AAEX;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;oCACoC;AACpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,cAAc,GAAE,MAA0C,GAAG,UAAU,EAAE,CAY3H"}
package/dist/constants.js CHANGED
@@ -21,6 +21,10 @@ export const ASSETS_RUST_LIMITS = {
21
21
  maxMusicStartSec: 600,
22
22
  minSlideDurationSec: 5,
23
23
  maxSlideDurationSec: 60,
24
+ /** `upload_my_avatar`: the largest file, the largest side in px. Scale the image down
25
+ * on the client first (256 px JPEG fits well). */
26
+ avatarMaxBytes: 64 * 1024,
27
+ avatarMaxSide: 512,
24
28
  };
25
29
  /**
26
30
  * The public URL of a stored asset. Served from the raw host (`<id>.raw.icp0.io`), which
@@ -29,6 +33,14 @@ export const ASSETS_RUST_LIMITS = {
29
33
  export function assetUrl(canisterId, assetId) {
30
34
  return `https://${canisterId}.raw.icp0.io/asset/${assetId}`;
31
35
  }
36
+ /**
37
+ * The stable public URL of a principal's avatar: always their current one, 404 when they have
38
+ * none. Cached for 5 minutes, so a new avatar shows up for others within that time; add a
39
+ * query such as `?v=<asset id>` to see a fresh one at once.
40
+ */
41
+ export function avatarUrl(canisterId, owner) {
42
+ return `https://${canisterId}.raw.icp0.io/avatar/${owner}`;
43
+ }
32
44
  /** Splits `bytes` into chunks the canister accepts. An empty input is one empty chunk —
33
45
  * a session needs at least one. */
34
46
  export function splitIntoChunks(bytes, chunkSizeBytes = ASSETS_RUST_LIMITS.chunkSizeBytes) {
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@ export { AssetsRustUploadsActor } from "./actors/uploads-actor.js";
4
4
  export { AssetsRustSalonActor } from "./actors/salon-actor.js";
5
5
  export { AssetsRustWorkerActor } from "./actors/worker-actor.js";
6
6
  export { AssetsRustAssetsActor } from "./actors/assets-actor.js";
7
- export { ASSETS_RUST_LIMITS, DEFAULT_HOST, assetUrl, splitIntoChunks } from "./constants.js";
7
+ export { AssetsRustAvatarActor } from "./actors/avatar-actor.js";
8
+ export { ASSETS_RUST_LIMITS, DEFAULT_HOST, assetUrl, avatarUrl, splitIntoChunks } from "./constants.js";
8
9
  export type { AssetInfoView, WorkerView, ReservationView, TranscodingKindView, RenderModeView, StartUploadInput, UploadAssetInput, UploadImageInput, ReserveTranscodingUploadInput, ReserveRenderInput, SystemStartUploadInput, HttpGetInput, HttpResponseView, UploadProgress, ChunkedUploadOptions, } from "./interfaces.js";
9
10
  export { mapAssetInfoView, mapWorkerView, mapReservationView, mapHttpResponseView, toCandidTranscodingKind, toCandidRenderMode, toCandidOpt, } from "./mappers.js";
10
11
  export type { AssetInfo, WorkerEntry, ReserveTranscodingUpload, TranscodingKind, RenderMode, HttpRequest, HttpResponse, StreamingCallbackToken, StreamingCallbackHttpResponse, StreamingStrategy, } from "../declarations/sultana-assets-rust/sultana-assets-rust.did.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC7F,YAAY,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACH,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EACR,SAAS,EACT,WAAW,EACX,wBAAwB,EACxB,eAAe,EACf,UAAU,EACV,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,6BAA6B,EAC7B,iBAAiB,GACpB,MAAM,gEAAgE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxG,YAAY,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACH,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EACR,SAAS,EACT,WAAW,EACX,wBAAwB,EACxB,eAAe,EACf,UAAU,EACV,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,6BAA6B,EAC7B,iBAAiB,GACpB,MAAM,gEAAgE,CAAC"}
package/dist/index.js CHANGED
@@ -4,5 +4,6 @@ export { AssetsRustUploadsActor } from "./actors/uploads-actor.js";
4
4
  export { AssetsRustSalonActor } from "./actors/salon-actor.js";
5
5
  export { AssetsRustWorkerActor } from "./actors/worker-actor.js";
6
6
  export { AssetsRustAssetsActor } from "./actors/assets-actor.js";
7
- export { ASSETS_RUST_LIMITS, DEFAULT_HOST, assetUrl, splitIntoChunks } from "./constants.js";
7
+ export { AssetsRustAvatarActor } from "./actors/avatar-actor.js";
8
+ export { ASSETS_RUST_LIMITS, DEFAULT_HOST, assetUrl, avatarUrl, splitIntoChunks } from "./constants.js";
8
9
  export { mapAssetInfoView, mapWorkerView, mapReservationView, mapHttpResponseView, toCandidTranscodingKind, toCandidRenderMode, toCandidOpt, } from "./mappers.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsm-mit/sultana-assets-rust-package",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Wrapper TypeScript package for the sultana-assets-rust canister: chunked and single-shot uploads, salon images and transcoding reservations, the worker upload path, reads and asset URLs.",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -28,6 +28,9 @@
28
28
  "test-suite": "bash scripts/run-tests.sh tests/test-suite.ts --lease 3",
29
29
  "test-smoke": "bash scripts/run-tests.sh tests/test-smoke.ts --lease 3",
30
30
  "test-upgrade": "bash scripts/run-tests.sh tests/test-upgrade.ts --lease 6",
31
+ "test-suite-local": "bash scripts/run-tests-local.sh tests/test-suite.ts",
32
+ "test-smoke-local": "bash scripts/run-tests-local.sh tests/test-smoke.ts",
33
+ "test-upgrade-local": "bash scripts/run-tests-local.sh tests/test-upgrade.ts",
31
34
  "redeploy": "bash scripts/redeploy.sh",
32
35
  "sandbox": "npx tsx sandbox/main.ts",
33
36
  "stamp-canister": "bash scripts/stamp-canister-commit.sh"
@@ -55,6 +58,6 @@
55
58
  "sultanaAssets": {
56
59
  "repo": "sultana-assets-rust",
57
60
  "repoUrl": "https://github.com/JSM-Sultana/sultana-assets-rust",
58
- "commit": "2360e780f543b7bb4eb2d67cc9e6f63d784bd404"
61
+ "commit": "d63f0d1ebcfa98c953ae7f68b5331e81966cec8b"
59
62
  }
60
63
  }