@kehto/services 0.22.0 → 0.23.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/dist/index.js CHANGED
@@ -1339,11 +1339,11 @@ function hasSourceRef(source) {
1339
1339
  }
1340
1340
  function canonicalizeSessionId(state, windowId, preferredSessionId) {
1341
1341
  const trimmed = typeof preferredSessionId === "string" ? preferredSessionId.trim() : "";
1342
- const hint = trimmed || `session-${++state.sessionCounter}`;
1343
- if (!state.sessionRegistry.has(hint)) return hint;
1342
+ const hint2 = trimmed || `session-${++state.sessionCounter}`;
1343
+ if (!state.sessionRegistry.has(hint2)) return hint2;
1344
1344
  let next;
1345
1345
  do {
1346
- next = `${windowId}:${hint}:${++state.sessionCounter}`;
1346
+ next = `${windowId}:${hint2}:${++state.sessionCounter}`;
1347
1347
  } while (state.sessionRegistry.has(next));
1348
1348
  return next;
1349
1349
  }
@@ -2167,14 +2167,14 @@ var ConfigServiceController = class {
2167
2167
  }
2168
2168
  }
2169
2169
  handleOpenSettings(windowId, message, send) {
2170
- const record = this.schemas.get(windowId);
2171
- if (!record) {
2170
+ const record2 = this.schemas.get(windowId);
2171
+ if (!record2) {
2172
2172
  sendSchemaError(send, "no-schema", "no configuration schema is registered");
2173
2173
  return;
2174
2174
  }
2175
2175
  let values;
2176
2176
  try {
2177
- values = resolveConfigValues(record.schema, this.options.getValues(windowId));
2177
+ values = resolveConfigValues(record2.schema, this.options.getValues(windowId));
2178
2178
  } catch (cause) {
2179
2179
  sendSchemaError(
2180
2180
  send,
@@ -2183,9 +2183,9 @@ var ConfigServiceController = class {
2183
2183
  );
2184
2184
  return;
2185
2185
  }
2186
- const section = message.section && schemaHasSection(record.schema, message.section) ? message.section : void 0;
2186
+ const section = message.section && schemaHasSection(record2.schema, message.section) ? message.section : void 0;
2187
2187
  this.options.openSettings?.(windowId, section, {
2188
- schema: structuredClone(record.schema),
2188
+ schema: structuredClone(record2.schema),
2189
2189
  values,
2190
2190
  commit: (next) => this.commitValues(windowId, next)
2191
2191
  });
@@ -3711,371 +3711,256 @@ function toErrorMessage3(err2) {
3711
3711
  }
3712
3712
 
3713
3713
  // src/intent-service.ts
3714
- var INTENT_SERVICE_VERSION = "1.0.0";
3715
3714
  var INTENT_DESCRIPTOR = {
3716
3715
  name: "intent",
3717
- version: INTENT_SERVICE_VERSION,
3718
- description: "NAP-INTENT archetype intent dispatch \u2014 invoke/available/handlers"
3716
+ version: "1.0.0",
3717
+ description: "NAP-INTENT convention dispatch"
3719
3718
  };
3720
- function toErrorMessage4(err2) {
3721
- if (err2 instanceof Error) return err2.message;
3722
- if (typeof err2 === "string") return err2;
3723
- return "intent request failed";
3724
- }
3725
- function hasOwn(value, key) {
3719
+ var CONVENTION = /^napplet:([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/;
3720
+ var HINT_ADDRESS = /^35129:[a-f0-9]{64}:.+$/;
3721
+ function own(value, key) {
3726
3722
  return Object.prototype.hasOwnProperty.call(value, key);
3727
3723
  }
3728
- function validateIntentRequest(value) {
3729
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
3730
- return { error: "invalid request" };
3731
- }
3732
- const request = value;
3733
- if (typeof request.archetype !== "string" || request.archetype.length === 0) {
3734
- return { error: "invalid request" };
3735
- }
3736
- const action = request.action === void 0 ? "open" : request.action;
3737
- if (typeof action !== "string" || action.length === 0) {
3738
- return { error: "invalid request" };
3739
- }
3740
- if (request.convention !== void 0 && (typeof request.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(request.convention))) {
3741
- return { error: "invalid convention" };
3742
- }
3743
- const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
3744
- if (Object.keys(request).some((key) => !allowedKeys.includes(key))) {
3745
- return { error: "invoke rejected" };
3746
- }
3747
- if (hasOwn(request, "handler") && request.handler !== void 0 && (typeof request.handler !== "string" || request.handler.length === 0)) {
3748
- return { error: "invoke rejected" };
3749
- }
3750
- let behavior;
3751
- if (hasOwn(request, "behavior") && request.behavior !== void 0) {
3752
- if (typeof request.behavior !== "object" || request.behavior === null || Array.isArray(request.behavior)) {
3753
- return { error: "invoke rejected" };
3754
- }
3755
- const record = request.behavior;
3756
- if (Object.keys(record).some((key) => key !== "focus" && key !== "newWindow" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "newWindow") && typeof record.newWindow !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
3757
- return { error: "invoke rejected" };
3758
- }
3759
- behavior = {
3760
- ...record.focus === void 0 ? {} : { focus: record.focus },
3761
- ...record.newWindow === void 0 ? {} : { newWindow: record.newWindow },
3762
- ...record.reuse === void 0 ? {} : { reuse: record.reuse }
3763
- };
3764
- }
3765
- return {
3766
- request: {
3767
- archetype: request.archetype,
3768
- action,
3769
- ...typeof request.convention === "string" ? { convention: request.convention } : {},
3770
- ...hasOwn(request, "payload") ? { payload: request.payload } : {},
3771
- ...typeof request.handler === "string" ? { handler: request.handler } : {},
3772
- ...behavior === void 0 ? {} : { behavior }
3773
- }
3774
- };
3775
- }
3776
- function settleResolverCall(call, send, resultType, id, onValue) {
3777
- let pending;
3778
- try {
3779
- pending = Promise.resolve(call());
3780
- } catch (error) {
3781
- send({ type: resultType, id, error: toErrorMessage4(error) });
3782
- return;
3783
- }
3784
- pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
3785
- }
3786
- function rejectedResult(request, error) {
3787
- return {
3788
- ok: false,
3789
- archetype: request.archetype,
3790
- action: request.action ?? "open",
3791
- handled: false,
3792
- error
3793
- };
3724
+ function record(value) {
3725
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3726
+ }
3727
+ function hint(value) {
3728
+ const source = record(value);
3729
+ if (!source || !own(source, "address") || typeof source.address !== "string" || !HINT_ADDRESS.test(source.address)) return void 0;
3730
+ if (Object.keys(source).some((key) => key !== "address" && key !== "relays")) return void 0;
3731
+ if (source.relays !== void 0 && (!Array.isArray(source.relays) || source.relays.some((relay) => typeof relay !== "string"))) return void 0;
3732
+ return Object.freeze({
3733
+ address: source.address,
3734
+ ...source.relays === void 0 ? {} : { relays: Object.freeze([...source.relays]) }
3735
+ });
3794
3736
  }
3795
- function rejectInvoke(send, id, request, error = "invoke rejected") {
3796
- send({
3797
- type: "intent.invoke.result",
3798
- id,
3799
- result: rejectedResult(request, error)
3737
+ function validateRequest(value) {
3738
+ const source = record(value);
3739
+ if (!source || !own(source, "archetype") || !own(source, "action") || !own(source, "convention")) return void 0;
3740
+ if (Object.keys(source).some((key) => !["archetype", "action", "convention", "payload", "handler", "handlerHint", "behavior"].includes(key))) return void 0;
3741
+ if (typeof source.archetype !== "string" || typeof source.action !== "string" || typeof source.convention !== "string") return void 0;
3742
+ const match = CONVENTION.exec(source.convention);
3743
+ if (!match || match[1] !== source.archetype || match[2] !== source.action) return void 0;
3744
+ if (source.handler !== void 0 && (typeof source.handler !== "string" || source.handler.length === 0)) return void 0;
3745
+ const parsedHint = source.handlerHint === void 0 ? void 0 : hint(source.handlerHint);
3746
+ if (source.handlerHint !== void 0 && !parsedHint) return void 0;
3747
+ const rawBehavior = source.behavior === void 0 ? void 0 : record(source.behavior);
3748
+ if (source.behavior !== void 0 && (!rawBehavior || Object.keys(rawBehavior).some((key) => key !== "focus" && key !== "reuse") || rawBehavior.focus !== void 0 && typeof rawBehavior.focus !== "boolean" || rawBehavior.reuse !== void 0 && typeof rawBehavior.reuse !== "boolean")) return void 0;
3749
+ return Object.freeze({
3750
+ archetype: source.archetype,
3751
+ action: source.action,
3752
+ convention: source.convention,
3753
+ ...own(source, "payload") ? { payload: source.payload } : {},
3754
+ ...source.handler === void 0 ? {} : { handler: source.handler },
3755
+ ...parsedHint === void 0 ? {} : { handlerHint: parsedHint },
3756
+ ...rawBehavior === void 0 ? {} : { behavior: Object.freeze({
3757
+ ...rawBehavior.focus === void 0 ? {} : { focus: rawBehavior.focus },
3758
+ ...rawBehavior.reuse === void 0 ? {} : { reuse: rawBehavior.reuse }
3759
+ }) }
3800
3760
  });
3801
3761
  }
3762
+ function reject(send, id, error = "invoke rejected") {
3763
+ send({ type: "intent.invoke.result", id, result: { ok: false, error } });
3764
+ }
3802
3765
  function createIntentService(options) {
3803
- if (!options || typeof options.resolver !== "object" || options.resolver === null) {
3804
- throw new Error("createIntentService: options.resolver is required");
3766
+ if (!options || !options.resolver || typeof options.resolveSender !== "function") {
3767
+ throw new Error("createIntentService: resolver and resolveSender are required");
3805
3768
  }
3806
- const { resolver } = options;
3807
3769
  let runtimeContext;
3808
- let unsubscribeChanged;
3809
- function handleInvoke(windowId, msg, send) {
3810
- const m = msg;
3811
- const id = m.id ?? "";
3812
- const validation = validateIntentRequest(m.request);
3813
- if ("error" in validation) {
3814
- const raw = typeof m.request === "object" && m.request !== null ? m.request : {};
3815
- rejectInvoke(
3816
- send,
3817
- id,
3818
- {
3819
- archetype: typeof raw.archetype === "string" ? raw.archetype : "",
3820
- action: typeof raw.action === "string" ? raw.action : "open"
3821
- },
3822
- validation.error
3823
- );
3824
- return;
3825
- }
3826
- const sender = runtimeContext?.resolveDTag(windowId);
3827
- if (!sender) {
3828
- rejectInvoke(send, id, validation.request);
3829
- return;
3830
- }
3831
- let pending;
3770
+ let unsubscribe;
3771
+ const { resolver, resolveSender } = options;
3772
+ function invoke2(windowId, message, send) {
3773
+ const envelope = message;
3774
+ const id = typeof envelope.id === "string" ? envelope.id : "";
3775
+ const request = validateRequest(envelope.request);
3776
+ let sender;
3832
3777
  try {
3833
- pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
3778
+ sender = resolveSender(windowId);
3834
3779
  } catch {
3835
- rejectInvoke(send, id, validation.request);
3836
- return;
3780
+ return reject(send, id);
3837
3781
  }
3838
- void pending.then(
3839
- (result2) => {
3840
- send({
3841
- type: "intent.invoke.result",
3842
- id,
3843
- result: result2
3844
- });
3845
- },
3846
- () => {
3847
- rejectInvoke(send, id, validation.request);
3848
- }
3782
+ if (!request || !sender) return reject(send, id, request ? "invoke rejected" : "invalid convention");
3783
+ void Promise.resolve().then(() => resolver.invoke(request, { sender, sourceWindowId: windowId })).then(
3784
+ (result2) => send({ type: "intent.invoke.result", id, result: result2 }),
3785
+ () => reject(send, id)
3849
3786
  ).catch(() => {
3850
3787
  });
3851
3788
  }
3852
- function handleAvailable(msg, send) {
3853
- const m = msg;
3854
- const id = m.id ?? "";
3855
- if (typeof m.archetype !== "string" || m.archetype.length === 0) {
3789
+ function availability(message, send) {
3790
+ const envelope = message;
3791
+ const id = typeof envelope.id === "string" ? envelope.id : "";
3792
+ if (typeof envelope.archetype !== "string" || envelope.archetype.length === 0) {
3856
3793
  send({ type: "intent.available.result", id, error: "invalid archetype" });
3857
3794
  return;
3858
3795
  }
3859
- const archetype = m.archetype;
3860
- settleResolverCall(
3861
- () => resolver.available(archetype),
3862
- send,
3863
- "intent.available.result",
3864
- id,
3865
- (availability) => ({ type: "intent.available.result", id, availability })
3866
- );
3796
+ const archetype = envelope.archetype;
3797
+ void Promise.resolve().then(() => resolver.available(archetype)).then(
3798
+ (value) => send({ type: "intent.available.result", id, availability: value }),
3799
+ () => send({ type: "intent.available.result", id, error: "catalog unavailable" })
3800
+ ).catch(() => {
3801
+ });
3867
3802
  }
3868
- function handleHandlers(msg, send) {
3869
- const m = msg;
3870
- const id = m.id ?? "";
3871
- settleResolverCall(
3872
- () => resolver.handlers(),
3873
- send,
3874
- "intent.handlers.result",
3875
- id,
3876
- (handlers) => ({ type: "intent.handlers.result", id, handlers })
3877
- );
3803
+ function handlers(message, send) {
3804
+ const envelope = message;
3805
+ const id = typeof envelope.id === "string" ? envelope.id : "";
3806
+ void Promise.resolve().then(() => resolver.handlers()).then(
3807
+ (value) => send({ type: "intent.handlers.result", id, handlers: value }),
3808
+ () => send({ type: "intent.handlers.result", id, error: "catalog unavailable" })
3809
+ ).catch(() => {
3810
+ });
3878
3811
  }
3879
3812
  return {
3880
3813
  descriptor: INTENT_DESCRIPTOR,
3881
3814
  onRegistered(context) {
3882
- try {
3883
- unsubscribeChanged?.();
3884
- } catch {
3885
- }
3815
+ unsubscribe?.();
3886
3816
  runtimeContext = context;
3887
- unsubscribeChanged = resolver.onChanged?.((availability) => {
3817
+ unsubscribe = resolver.onChanged?.((item) => {
3888
3818
  const current = runtimeContext;
3889
3819
  if (!current) return;
3890
- const message = { type: "intent.changed", availability };
3891
3820
  for (const windowId of current.listWindowIds()) {
3892
- current.sendToEligibleNapplet(windowId, message);
3821
+ current.sendToEligibleNapplet(windowId, { type: "intent.changed", availability: item });
3893
3822
  }
3894
3823
  });
3895
3824
  },
3896
3825
  onUnregistered() {
3897
- const unsubscribe = unsubscribeChanged;
3898
- unsubscribeChanged = void 0;
3826
+ unsubscribe?.();
3827
+ unsubscribe = void 0;
3899
3828
  runtimeContext = void 0;
3900
- try {
3901
- unsubscribe?.();
3902
- } catch {
3903
- }
3904
3829
  },
3905
3830
  handleMessage(windowId, message, send) {
3906
- switch (message.type) {
3907
- case "intent.invoke":
3908
- handleInvoke(windowId, message, send);
3909
- return;
3910
- case "intent.available":
3911
- handleAvailable(message, send);
3912
- return;
3913
- case "intent.handlers":
3914
- handleHandlers(message, send);
3915
- return;
3916
- default:
3917
- return;
3918
- }
3831
+ if (message.type === "intent.invoke") return invoke2(windowId, message, send);
3832
+ if (message.type === "intent.available") return availability(message, send);
3833
+ if (message.type === "intent.handlers") return handlers(message, send);
3919
3834
  }
3920
3835
  };
3921
3836
  }
3922
3837
 
3923
3838
  // src/catalog-intent-resolver.ts
3924
- function candidatesFor(catalog, archetype, defaultHandler) {
3925
- const candidates = [];
3926
- for (const entry of catalog) {
3927
- const support = entry.archetypes[archetype];
3928
- if (!support) continue;
3929
- candidates.push({
3930
- dTag: entry.dTag,
3931
- ...entry.title === void 0 ? {} : { title: entry.title },
3932
- actions: [...support.actions],
3933
- conventions: [...support.conventions],
3934
- ...entry.dTag === defaultHandler ? { isDefault: true } : {}
3935
- });
3936
- }
3937
- return candidates;
3839
+ var CONVENTION2 = /^napplet:([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/;
3840
+ function rejected(error) {
3841
+ return { ok: false, error };
3842
+ }
3843
+ function candidateFor(entry, archetype, defaultId) {
3844
+ const support = entry.archetypes[archetype];
3845
+ if (!support || support.contracts.length === 0) return void 0;
3846
+ const contracts = support.contracts.map((contract) => Object.freeze({
3847
+ convention: contract.convention,
3848
+ params: Object.freeze([...contract.params])
3849
+ }));
3850
+ const conventions = [...new Set(contracts.map((contract) => contract.convention))];
3851
+ const actions = [...new Set(conventions.map((convention) => CONVENTION2.exec(convention)?.[2]).filter((value) => value !== void 0))];
3852
+ if (actions.length === 0) return void 0;
3853
+ return Object.freeze({
3854
+ id: entry.id,
3855
+ ...entry.title === void 0 ? {} : { title: entry.title },
3856
+ actions: Object.freeze(actions),
3857
+ conventions: Object.freeze(conventions),
3858
+ contracts: Object.freeze(contracts),
3859
+ ...entry.id === defaultId ? { isDefault: true } : {}
3860
+ });
3938
3861
  }
3939
- function reject(request, error) {
3940
- return {
3941
- ok: false,
3942
- archetype: request.archetype,
3943
- action: request.action ?? "open",
3944
- handled: false,
3945
- error
3946
- };
3862
+ function candidatesFor(catalog, archetype, defaultId) {
3863
+ return catalog.map((entry) => candidateFor(entry, archetype, defaultId)).filter((candidate) => candidate !== void 0);
3947
3864
  }
3948
- async function availabilityForCatalog(options, archetype) {
3949
- const catalog = await options.loadCatalog();
3950
- const defaultHandler = options.getDefaultHandler?.(archetype);
3951
- const candidates = candidatesFor(catalog, archetype, defaultHandler);
3952
- return {
3953
- archetype,
3954
- available: candidates.length > 0,
3955
- candidates,
3956
- hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
3957
- };
3865
+ function compatibleFor(request, candidates) {
3866
+ return candidates.filter((candidate) => candidate.contracts.some((contract) => contract.convention === request.convention));
3958
3867
  }
3959
- async function chooseCompatible(options, request, candidates, sender) {
3960
- if (!options.chooseHandler) {
3961
- return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
3962
- }
3963
- const picked = await options.chooseHandler(request.archetype, candidates, sender);
3964
- if (picked === void 0) return { error: "user cancelled" };
3965
- const candidate = candidates.find((item) => item.dTag === picked);
3966
- return candidate ? { candidate } : { error: "invoke rejected" };
3967
- }
3968
- async function pickHandler(options, request, compatible, sender) {
3969
- const preference = request.handler;
3970
- if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
3971
- const candidate = compatible.find((item) => item.dTag === preference);
3972
- if (!candidate || !options.authorizeExplicitHandler) return { error: "invoke rejected" };
3973
- let authorized = false;
3868
+ async function choose(options, request, candidates, sender) {
3869
+ if (!options.chooseHandler) return rejected(request.handler === "choose" ? "user cancelled" : "invoke rejected");
3870
+ const id = await options.chooseHandler(request.archetype, candidates, sender);
3871
+ if (id === void 0) return rejected("user cancelled");
3872
+ return candidates.find((candidate) => candidate.id === id) ?? rejected("invoke rejected");
3873
+ }
3874
+ async function select(options, request, candidates, context) {
3875
+ const { sender } = context;
3876
+ if (request.handler && request.handler !== "default" && request.handler !== "choose") {
3877
+ const candidate = candidates.find((item) => item.id === request.handler);
3878
+ if (!candidate || !options.authorizeExplicitHandler) return rejected("invoke rejected");
3974
3879
  try {
3975
- authorized = await options.authorizeExplicitHandler(
3976
- sender,
3977
- preference,
3978
- request,
3979
- candidate
3980
- );
3880
+ return await options.authorizeExplicitHandler(sender, request.handler, request, candidate, context) ? candidate : rejected("invoke rejected");
3981
3881
  } catch {
3982
- return { error: "invoke rejected" };
3882
+ return rejected("invoke rejected");
3983
3883
  }
3984
- return authorized ? { candidate } : { error: "invoke rejected" };
3985
3884
  }
3986
- if (preference === "choose") {
3987
- return chooseCompatible(options, request, compatible, sender);
3988
- }
3989
- const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
3990
- if (preference === "default") {
3991
- return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
3885
+ if (request.handler === "choose") return choose(options, request, candidates, sender);
3886
+ const defaultCandidate = candidates.find((candidate) => candidate.isDefault);
3887
+ if (defaultCandidate) return defaultCandidate;
3888
+ if (request.handlerHint && options.resolveHandlerHint) {
3889
+ try {
3890
+ const id = await options.resolveHandlerHint(request.handlerHint, candidates);
3891
+ const candidate = candidates.find((item) => item.id === id);
3892
+ if (candidate) return candidate;
3893
+ } catch {
3894
+ }
3992
3895
  }
3993
- if (defaultCandidate) return { candidate: defaultCandidate };
3994
- if (compatible.length === 1) return { candidate: compatible[0] };
3995
- return chooseCompatible(options, request, compatible, sender);
3896
+ if (candidates.length === 1) return candidates[0];
3897
+ return choose(options, request, candidates, sender);
3996
3898
  }
3997
- async function invokeFromCatalog(options, request, context) {
3899
+ async function availabilityFor(options, archetype) {
3998
3900
  const catalog = await options.loadCatalog();
3999
- const defaultHandler = options.getDefaultHandler?.(request.archetype);
4000
- const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
4001
- if (candidates.length === 0) return reject(request, "no handler");
4002
- const action = request.action ?? "open";
4003
- const actionCompatible = candidates.filter((candidate) => candidate.actions.includes(action));
4004
- if (actionCompatible.length === 0) return reject(request, "unsupported action");
4005
- const compatible = request.convention === void 0 ? actionCompatible : actionCompatible.filter((candidate) => candidate.conventions.includes(request.convention));
4006
- if (compatible.length === 0) return reject(request, "unsupported convention");
4007
- const sender = context.sender;
4008
- if (typeof sender !== "string" || sender.length === 0) {
4009
- return reject(request, "invoke rejected");
4010
- }
4011
- const selected = await pickHandler(options, request, compatible, sender);
4012
- if ("error" in selected) return reject(request, selected.error);
4013
- const convention = request.convention ?? selected.candidate.conventions.find((value) => {
4014
- const match = /^napplet:[^/?#\s]+\/([^/?#\s]+)$/.exec(value);
4015
- return match?.[1] === action;
3901
+ const defaultId = options.getDefaultHandler?.(archetype);
3902
+ const candidates = candidatesFor(catalog, archetype, defaultId);
3903
+ return Object.freeze({
3904
+ archetype,
3905
+ available: candidates.length > 0,
3906
+ candidates: Object.freeze(candidates),
3907
+ hasDefault: candidates.some((candidate) => candidate.isDefault)
4016
3908
  });
4017
- if (!convention) return reject(request, "unsupported convention");
3909
+ }
3910
+ async function invoke(options, request, context) {
3911
+ const match = CONVENTION2.exec(request.convention);
3912
+ if (!match || match[1] !== request.archetype || match[2] !== request.action) return rejected("invalid convention");
3913
+ if (!context.sender) return rejected("invoke rejected");
3914
+ const catalog = await options.loadCatalog();
3915
+ const candidates = candidatesFor(catalog, request.archetype, options.getDefaultHandler?.(request.archetype));
3916
+ if (candidates.length === 0) return rejected("no handler");
3917
+ const compatible = compatibleFor(request, candidates);
3918
+ if (compatible.length === 0) return rejected(candidates.some((candidate) => candidate.actions.includes(request.action)) ? "unsupported convention" : "unsupported action");
3919
+ const selected = await select(options, request, compatible, context);
3920
+ if ("ok" in selected) return selected;
4018
3921
  const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
4019
3922
  ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
4020
- ...request.behavior.newWindow === void 0 ? {} : { newWindow: request.behavior.newWindow },
4021
3923
  ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
4022
3924
  });
4023
3925
  const params = Object.freeze({
4024
- handler: selected.candidate.dTag,
4025
- sender,
3926
+ handler: selected.id,
3927
+ sender: context.sender,
4026
3928
  archetype: request.archetype,
4027
- action,
4028
- convention,
3929
+ action: request.action,
3930
+ convention: request.convention,
4029
3931
  ...request.payload === void 0 ? {} : { payload: request.payload },
4030
- ...behavior === void 0 ? {} : { behavior }
3932
+ ...behavior === void 0 ? {} : { behavior },
3933
+ ...context.sourceWindowId === void 0 ? {} : { sourceWindowId: context.sourceWindowId }
4031
3934
  });
4032
- let target;
4033
3935
  try {
4034
- target = await options.targets.dispatch(params);
3936
+ const accepted = options.targets.accept(params);
3937
+ if (!accepted || !(accepted.completion instanceof Promise)) return rejected("invoke rejected");
3938
+ void accepted.completion.catch(() => {
3939
+ });
4035
3940
  } catch {
4036
- return reject(request, "invoke failed");
3941
+ return rejected("invoke rejected");
4037
3942
  }
4038
- if (!target || typeof target.windowId !== "string" || target.windowId.length === 0) {
4039
- return reject(request, "invoke failed");
4040
- }
4041
- return {
4042
- ok: true,
4043
- archetype: request.archetype,
4044
- action,
4045
- handled: true,
4046
- handler: selected.candidate.dTag,
4047
- windowId: target.windowId,
4048
- convention
4049
- };
3943
+ return { ok: true, archetype: request.archetype, action: request.action, convention: request.convention, handler: selected.id };
4050
3944
  }
4051
3945
  function createCatalogIntentResolver(options) {
4052
- if (!options || typeof options.loadCatalog !== "function") {
4053
- throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
4054
- }
4055
- if (!options.targets || typeof options.targets.dispatch !== "function") {
4056
- throw new Error("createCatalogIntentResolver: options.targets is required");
4057
- }
3946
+ if (!options || typeof options.loadCatalog !== "function") throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
3947
+ if (!options.targets || typeof options.targets.accept !== "function") throw new Error("createCatalogIntentResolver: options.targets is required");
4058
3948
  const listeners = /* @__PURE__ */ new Set();
4059
- const availabilityFor = (archetype) => availabilityForCatalog(options, archetype);
4060
- async function handlers() {
4061
- const catalog = await options.loadCatalog();
4062
- const archetypes = /* @__PURE__ */ new Set();
4063
- for (const entry of catalog) {
4064
- for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
4065
- }
4066
- return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
4067
- }
4068
3949
  return {
4069
- invoke: (request, context) => invokeFromCatalog(options, request, context),
4070
- available: availabilityFor,
4071
- handlers,
3950
+ invoke: (request, context) => invoke(options, request, context),
3951
+ available: (archetype) => availabilityFor(options, archetype),
3952
+ async handlers() {
3953
+ const catalog = await options.loadCatalog();
3954
+ const roles = /* @__PURE__ */ new Set();
3955
+ for (const entry of catalog) for (const role of Object.keys(entry.archetypes)) roles.add(role);
3956
+ return Promise.all([...roles].map((role) => availabilityFor(options, role)));
3957
+ },
4072
3958
  onChanged(listener) {
4073
3959
  listeners.add(listener);
4074
3960
  return () => listeners.delete(listener);
4075
3961
  },
4076
3962
  notifyChanged(archetype) {
4077
- if (listeners.size === 0) return;
4078
- void availabilityFor(archetype).then((availability) => {
3963
+ void availabilityFor(options, archetype).then((availability) => {
4079
3964
  for (const listener of listeners) listener(availability);
4080
3965
  });
4081
3966
  }
@@ -4091,21 +3976,24 @@ function actionFromConvention(convention) {
4091
3976
  return match[2];
4092
3977
  }
4093
3978
  function manifestToIntentCatalogEntry(manifest) {
4094
- const archetypes = {};
4095
- for (const { slug, convention } of manifest.archetypes) {
3979
+ const archetypes = /* @__PURE__ */ Object.create(null);
3980
+ for (const { slug, convention, params } of manifest.archetypes) {
4096
3981
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
4097
3982
  throw new TypeError("manifest archetype slug is invalid");
4098
3983
  }
4099
- const action = actionFromConvention(convention);
3984
+ actionFromConvention(convention);
3985
+ if (!convention.startsWith(`napplet:${slug}/`)) {
3986
+ throw new TypeError("manifest archetype must match its convention role");
3987
+ }
4100
3988
  const support = archetypes[slug] ??= {
4101
- actions: [],
4102
- conventions: []
3989
+ contracts: []
4103
3990
  };
4104
- if (!support.actions.includes(action)) support.actions.push(action);
4105
- if (!support.conventions.includes(convention)) support.conventions.push(convention);
3991
+ if (!support.contracts.some((contract) => contract.convention === convention)) {
3992
+ support.contracts.push({ convention, params: [...params] });
3993
+ }
4106
3994
  }
4107
3995
  return {
4108
- dTag: manifest.dTag,
3996
+ id: manifest.catalogId,
4109
3997
  ...manifest.title === void 0 ? {} : { title: manifest.title },
4110
3998
  archetypes
4111
3999
  };
@@ -4153,7 +4041,7 @@ function createCvmService(options) {
4153
4041
  const m = msg;
4154
4042
  const id = m.id ?? "";
4155
4043
  void transport.discover(m.query).then((servers) => send({ type: "cvm.discover.result", id, servers })).catch(
4156
- (err2) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage5(err2) })
4044
+ (err2) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage4(err2) })
4157
4045
  );
4158
4046
  }
4159
4047
  function handleRequest(windowId, msg, send) {
@@ -4169,7 +4057,7 @@ function createCvmService(options) {
4169
4057
  }
4170
4058
  openSession(windowId, m.server, send);
4171
4059
  void transport.request(m.server, m.message, m.options).then((message) => send({ type: "cvm.request.result", id, message })).catch(
4172
- (err2) => send({ type: "cvm.request.result", id, error: toErrorMessage5(err2) })
4060
+ (err2) => send({ type: "cvm.request.result", id, error: toErrorMessage4(err2) })
4173
4061
  );
4174
4062
  }
4175
4063
  function handleClose(windowId, msg, send) {
@@ -4180,7 +4068,7 @@ function createCvmService(options) {
4180
4068
  return;
4181
4069
  }
4182
4070
  closeSession(windowId, m.server.pubkey);
4183
- void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err2) => send({ type: "cvm.close.result", id, error: toErrorMessage5(err2) }));
4071
+ void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err2) => send({ type: "cvm.close.result", id, error: toErrorMessage4(err2) }));
4184
4072
  }
4185
4073
  return {
4186
4074
  descriptor: CVM_DESCRIPTOR,
@@ -4218,7 +4106,7 @@ function createCvmService(options) {
4218
4106
  }
4219
4107
  };
4220
4108
  }
4221
- function toErrorMessage5(err2) {
4109
+ function toErrorMessage4(err2) {
4222
4110
  if (err2 instanceof Error) return err2.message;
4223
4111
  if (typeof err2 === "string") return err2;
4224
4112
  return "cvm request failed";
@@ -4414,15 +4302,15 @@ function createSerialService(options = {}) {
4414
4302
  const currentWindow = (windowId) => {
4415
4303
  const existing = windows.get(windowId);
4416
4304
  if (existing) return existing;
4417
- const record = {
4305
+ const record2 = {
4418
4306
  connections: /* @__PURE__ */ new Map(),
4419
4307
  pendingConnections: /* @__PURE__ */ new Set()
4420
4308
  };
4421
- windows.set(windowId, record);
4422
- return record;
4309
+ windows.set(windowId, record2);
4310
+ return record2;
4423
4311
  };
4424
- const emit = (windowId, record, connection, send, event) => {
4425
- if (!connection || windows.get(windowId) !== record || connection.state === "closed") return;
4312
+ const emit = (windowId, record2, connection, send, event) => {
4313
+ if (!connection || windows.get(windowId) !== record2 || connection.state === "closed") return;
4426
4314
  if (connection.state === "opening") {
4427
4315
  connection.bufferedEvents.push(event);
4428
4316
  return;
@@ -4433,8 +4321,8 @@ function createSerialService(options = {}) {
4433
4321
  descriptor: SERIAL_DESCRIPTOR,
4434
4322
  handleMessage(windowId, message, send) {
4435
4323
  const id = message.id ?? "";
4436
- const record = currentWindow(windowId);
4437
- const isCurrent = () => windows.get(windowId) === record;
4324
+ const record2 = currentWindow(windowId);
4325
+ const isCurrent = () => windows.get(windowId) === record2;
4438
4326
  const currentSend = (outbound) => {
4439
4327
  if (isCurrent()) send(outbound);
4440
4328
  };
@@ -4445,13 +4333,13 @@ function createSerialService(options = {}) {
4445
4333
  }
4446
4334
  const serialMessage = message;
4447
4335
  const connection = { state: "opening", bufferedEvents: [] };
4448
- record.pendingConnections.add(connection);
4449
- const context = createContext(windowId, (event) => emit(windowId, record, connection, currentSend, event));
4336
+ record2.pendingConnections.add(connection);
4337
+ const context = createContext(windowId, (event) => emit(windowId, record2, connection, currentSend, event));
4450
4338
  let pending;
4451
4339
  try {
4452
4340
  pending = Promise.resolve(options.open(serialMessage.request, context));
4453
4341
  } catch (error) {
4454
- record.pendingConnections.delete(connection);
4342
+ record2.pendingConnections.delete(connection);
4455
4343
  connection.state = "closed";
4456
4344
  currentSend({
4457
4345
  type: "serial.open.result",
@@ -4461,10 +4349,10 @@ function createSerialService(options = {}) {
4461
4349
  return;
4462
4350
  }
4463
4351
  void pending.then((result2) => {
4464
- record.pendingConnections.delete(connection);
4352
+ record2.pendingConnections.delete(connection);
4465
4353
  if (!isCurrent() || connection.state === "closed") return;
4466
4354
  connection.state = "open";
4467
- record.connections.set(result2.session.id, connection);
4355
+ record2.connections.set(result2.session.id, connection);
4468
4356
  currentSend({ type: "serial.open.result", id, session: result2.session });
4469
4357
  if (!isCurrent() || connection.state !== "open") return;
4470
4358
  for (const event of connection.bufferedEvents) {
@@ -4473,7 +4361,7 @@ function createSerialService(options = {}) {
4473
4361
  }
4474
4362
  connection.bufferedEvents.length = 0;
4475
4363
  }).catch((error) => {
4476
- record.pendingConnections.delete(connection);
4364
+ record2.pendingConnections.delete(connection);
4477
4365
  connection.state = "closed";
4478
4366
  currentSend({
4479
4367
  type: "serial.open.result",
@@ -4491,8 +4379,8 @@ function createSerialService(options = {}) {
4491
4379
  const serialMessage = message;
4492
4380
  const context = createContext(windowId, (event) => emit(
4493
4381
  windowId,
4494
- record,
4495
- record.connections.get(serialMessage.sessionId),
4382
+ record2,
4383
+ record2.connections.get(serialMessage.sessionId),
4496
4384
  currentSend,
4497
4385
  event
4498
4386
  ));
@@ -4510,14 +4398,14 @@ function createSerialService(options = {}) {
4510
4398
  return;
4511
4399
  }
4512
4400
  const serialMessage = message;
4513
- const connection = record.connections.get(serialMessage.sessionId);
4401
+ const connection = record2.connections.get(serialMessage.sessionId);
4514
4402
  if (connection) {
4515
4403
  connection.state = "closed";
4516
- record.connections.delete(serialMessage.sessionId);
4404
+ record2.connections.delete(serialMessage.sessionId);
4517
4405
  }
4518
4406
  const context = createContext(windowId, (event) => emit(
4519
4407
  windowId,
4520
- record,
4408
+ record2,
4521
4409
  connection,
4522
4410
  currentSend,
4523
4411
  event
@@ -4537,12 +4425,12 @@ function createSerialService(options = {}) {
4537
4425
  });
4538
4426
  },
4539
4427
  onWindowDestroyed(windowId) {
4540
- const record = windows.get(windowId);
4541
- if (record) {
4542
- for (const connection of record.connections.values()) connection.state = "closed";
4543
- for (const connection of record.pendingConnections) connection.state = "closed";
4544
- record.connections.clear();
4545
- record.pendingConnections.clear();
4428
+ const record2 = windows.get(windowId);
4429
+ if (record2) {
4430
+ for (const connection of record2.connections.values()) connection.state = "closed";
4431
+ for (const connection of record2.pendingConnections) connection.state = "closed";
4432
+ record2.connections.clear();
4433
+ record2.pendingConnections.clear();
4546
4434
  windows.delete(windowId);
4547
4435
  }
4548
4436
  options.destroyWindow?.(windowId);
@@ -5611,13 +5499,13 @@ function createCordnRelayCoordinatorClient(options) {
5611
5499
  const cursor = (cursors.get(gid) ?? 0) + 1;
5612
5500
  cursors.set(gid, cursor);
5613
5501
  const at = nowSeconds3();
5614
- const record = { cursor, gid, msg_64: input.msg_64, at };
5502
+ const record2 = { cursor, gid, msg_64: input.msg_64, at };
5615
5503
  const event = finalizeEvent2(
5616
5504
  {
5617
5505
  kind,
5618
5506
  created_at: at,
5619
5507
  tags: [["g", gid], ["p", ownerPubkey]],
5620
- content: JSON.stringify(record)
5508
+ content: JSON.stringify(record2)
5621
5509
  },
5622
5510
  options.ownerSecretKey
5623
5511
  );
@@ -5695,8 +5583,8 @@ function createCordnDmAdapter(options) {
5695
5583
  if (options.client) return options.client.messages(query);
5696
5584
  const gid = query.conversationId.startsWith("group:") ? query.conversationId.slice("group:".length) : query.conversationId;
5697
5585
  const fetched = await options.coordinator.FetchGroupMessages({ gid });
5698
- for (const record of fetched.messages) store.upsertMessage(
5699
- (options.decodeMessage ?? defaultDecodeMessage)(record, ownerPubkey),
5586
+ for (const record2 of fetched.messages) store.upsertMessage(
5587
+ (options.decodeMessage ?? defaultDecodeMessage)(record2, ownerPubkey),
5700
5588
  [ownerPubkey ?? "", gid]
5701
5589
  );
5702
5590
  return store.messages(query);
@@ -5705,8 +5593,8 @@ function createCordnDmAdapter(options) {
5705
5593
  if (options.client) return options.client.send(request);
5706
5594
  const encoded = (options.encodeMessage ?? ((req, pk) => defaultEncodeMessage(req, pk, options.defaultGroupId)))(request, ownerPubkey);
5707
5595
  const posted = await options.coordinator.PostGroupMessage({ msg_64: encoded.msg_64 });
5708
- const record = { ...posted, msg_64: encoded.msg_64 };
5709
- const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record, ownerPubkey), [ownerPubkey ?? "", encoded.gid]);
5596
+ const record2 = { ...posted, msg_64: encoded.msg_64 };
5597
+ const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record2, ownerPubkey), [ownerPubkey ?? "", encoded.gid]);
5710
5598
  return { ok: true, message: { ...message, status: "sent" } };
5711
5599
  },
5712
5600
  async subscribe(request, onMessage) {
@@ -5716,17 +5604,17 @@ function createCordnDmAdapter(options) {
5716
5604
  const gid = conversationId.startsWith("group:") ? conversationId.slice("group:".length) : conversationId;
5717
5605
  const fetched = await options.coordinator.FetchGroupMessages({ gid });
5718
5606
  let after = 0;
5719
- for (const record of fetched.messages) {
5720
- after = Math.max(after, record.cursor);
5721
- const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record, ownerPubkey), [ownerPubkey ?? "", gid]);
5607
+ for (const record2 of fetched.messages) {
5608
+ after = Math.max(after, record2.cursor);
5609
+ const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record2, ownerPubkey), [ownerPubkey ?? "", gid]);
5722
5610
  onMessage(message);
5723
5611
  }
5724
5612
  const subscription = await options.coordinator.SubscribeGroupMessages({ gid, after });
5725
5613
  const subscriptionId = `dm-cordn-${++cordnSubCounter}`;
5726
5614
  live.set(subscriptionId, { abort: () => subscription.abort("dm.unsubscribe"), conversationId });
5727
5615
  void (async () => {
5728
- for await (const record of subscription.stream) {
5729
- const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record, ownerPubkey), [ownerPubkey ?? "", gid]);
5616
+ for await (const record2 of subscription.stream) {
5617
+ const message = store.upsertMessage((options.decodeMessage ?? defaultDecodeMessage)(record2, ownerPubkey), [ownerPubkey ?? "", gid]);
5730
5618
  onMessage(message);
5731
5619
  }
5732
5620
  })();